From 3d53b0d6b8d9aa8e52c96c3ed6ac9f7dc1d660f4 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Wed, 2 Sep 2020 13:46:45 +0200 Subject: BBCode: Consolidate bb2html & bb2text + only convert ids in thread titles Fixing bb2html to only convert ids would complicate options a lot, adding a new formatting function to only convert ids would make sense, but then all formatting functions kind of look alike, so I figured a single bb_format() to support all use cases may be a better approach. Trigger for this was that people do (understandably) put [spoiler] in thread titles, and that should not be interpreted as the spoiler tag. --- lib/Multi/Feed.pm | 2 +- lib/VNDB/BBCode.pm | 132 +++++++++++++++++++--------------------- lib/VNDB/Func.pm | 2 +- lib/VNDB/Handler/Misc.pm | 2 +- lib/VNDB/Handler/Tags.pm | 2 +- lib/VNDB/Handler/Traits.pm | 2 +- lib/VNDB/Handler/VNPage.pm | 2 +- lib/VNWeb/Chars/Page.pm | 4 +- lib/VNWeb/Discussions/Thread.pm | 4 +- lib/VNWeb/Discussions/UPosts.pm | 2 +- lib/VNWeb/HTML.pm | 6 +- lib/VNWeb/Misc/BBCode.pm | 2 +- lib/VNWeb/Misc/History.pm | 2 +- lib/VNWeb/Misc/Reports.pm | 4 +- lib/VNWeb/Producers/Page.pm | 4 +- lib/VNWeb/Releases/Lib.pm | 2 +- lib/VNWeb/Releases/Page.pm | 4 +- lib/VNWeb/Reviews/Page.pm | 2 +- lib/VNWeb/Reviews/VNTab.pm | 2 +- lib/VNWeb/Staff/Page.pm | 4 +- lib/VNWeb/VN/Page.pm | 4 +- 21 files changed, 92 insertions(+), 98 deletions(-) (limited to 'lib') diff --git a/lib/Multi/Feed.pm b/lib/Multi/Feed.pm index be367bbc..ad52bace 100644 --- a/lib/Multi/Feed.pm +++ b/lib/Multi/Feed.pm @@ -114,7 +114,7 @@ sub write_atom { $x->end; } $x->tag(link => rel => 'alternate', type => 'text/html', href => config->{url}.$_->{id}, undef); - $x->tag('summary', type => 'html', bb2html $_->{summary}) if $_->{summary}; + $x->tag('summary', type => 'html', bb_format $_->{summary}) if $_->{summary}; $x->end('entry'); } diff --git a/lib/VNDB/BBCode.pm b/lib/VNDB/BBCode.pm index a5050eab..ecba3e58 100644 --- a/lib/VNDB/BBCode.pm +++ b/lib/VNDB/BBCode.pm @@ -5,7 +5,7 @@ use warnings; use Exporter 'import'; use TUWF::XML 'xml_escape'; -our @EXPORT = qw/bb2html bb2text bb_subst_links/; +our @EXPORT = qw/bb_format bb_subst_links/; # Supported BBCode: # [b] .. [/b] @@ -173,117 +173,111 @@ FINAL: } -# charspoil: -# 0/undef/missing: Output .. -# 1: Deprecated -# 2: Just output 'hidden by spoiler setting' message -# 3: Just output the spoilers, unmarked -sub bb2html { - my($input, $maxlength, $charspoil, $nobreak) = @_; +# Options: +# maxlength => 0/$n - truncate after $n visible characters +# inline => 0/1 - don't insert line breaks and don't format block elements +# +# One of: +# text => 0/1 - format as plain text, no tags +# onlyids => 0/1 - format as HTML, but only convert VNDBIDs, leave the rest alone (including [spoiler]s) +# default: format all to HTML. +# +# One of: +# delspoil => 0/1 - delete [spoiler] tags and its contents +# replacespoil => 0/1 - replace [spoiler] tags with a "hidden by spoiler settings" message +# keepsoil => 0/1 - keep the contents of spoiler tags without any special formatting +# default: format as .. +sub bb_format { + my($input, %opt) = @_; + $opt{delspoil} = 1 if $opt{text} && !$opt{keepspoil}; my $incode = 0; + my $inspoil = 0; my $rmnewline = 0; my $length = 0; my $ret = ''; # escapes, returns string, and takes care of $length and $maxlength; also # takes care to remove newlines and double spaces when necessary - my $e = sub { + my sub e { local $_ = shift; s/^\n// if $rmnewline && $rmnewline--; s/\n{5,}/\n\n/g if !$incode; s/ +/ /g if !$incode; $length += length $_; - if($maxlength && $length > $maxlength) { - $_ = substr($_, 0, $maxlength-$length); + if($opt{maxlength} && $length > $opt{maxlength}) { + $_ = substr($_, 0, $opt{maxlength}-$length); s/\W+\w*$//; # cleanly cut off on word boundary } - s/&/&/g; - s/>/>/g; - s//g if !$nobreak; - s/\n/ /g if $nobreak; + if(!$opt{text}) { + s/&/&/g; + s/>/>/g; + s//g if !$opt{inline}; + } + s/\n/ /g if $opt{inline}; $_; }; parse $input, sub { my($raw, $tag, @arg) = @_; - #$ret .= "$tag {$raw}\n"; - #return 1; + return 1 if $inspoil && $tag ne 'spoiler_end' && ($opt{delspoil} || $opt{replacespoil}); if($tag eq 'text') { - $ret .= $e->($raw); - - } elsif($tag eq 'b_start') { $ret .= ''; - } elsif($tag eq 'b_end') { $ret .= ''; - } elsif($tag eq 'i_start') { $ret .= ''; - } elsif($tag eq 'i_end') { $ret .= ''; - } elsif($tag eq 'u_start') { $ret .= ''; - } elsif($tag eq 'u_end') { $ret .= ''; - } elsif($tag eq 's_start') { $ret .= ''; - } elsif($tag eq 's_end') { $ret .= ''; - - } elsif($tag eq 'spoiler_start') { - $ret .= !$charspoil ? '' : - $charspoil == 2 ? '<hidden by spoiler settings>' : ''; + $ret .= e $raw; + } elsif($tag eq 'dblink') { + (my $link = $raw) =~ s/^d(\d+)\.(\d+)\.(\d+)$/d$1#$2.$3/; + $ret .= $opt{text} ? e $raw : sprintf '%s', $link, e $raw; + + } elsif($opt{idonly}) { + $ret .= e $raw; + + } elsif($tag eq 'b_start') { $ret .= $opt{text} ? e '*' : '' + } elsif($tag eq 'b_end') { $ret .= $opt{text} ? e '*' : '' + } elsif($tag eq 'i_start') { $ret .= $opt{text} ? e '/' : '' + } elsif($tag eq 'i_end') { $ret .= $opt{text} ? e '/' : '' + } elsif($tag eq 'u_start') { $ret .= $opt{text} ? e '_' : '' + } elsif($tag eq 'u_end') { $ret .= $opt{text} ? e '_' : '' + } elsif($tag eq 's_start') { $ret .= $opt{text} ? e '-' : '' + } elsif($tag eq 's_end') { $ret .= $opt{text} ? e '-' : '' } elsif($tag eq 'quote_start') { - $ret .= '
' if !$nobreak; + $ret .= $opt{text} || $opt{inline} ? e '"' : '
'; $rmnewline = 1; } elsif($tag eq 'quote_end') { - $ret .= '
' if !$nobreak; + $ret .= $opt{text} || $opt{inline} ? e '"' : '
'; $rmnewline = 1; } elsif($tag eq 'code_start') { - $ret .= '
' if !$nobreak;
+      $ret .= $opt{text} || $opt{inline} ? e '`' : '
';
       $rmnewline = 1;
       $incode = 1;
     } elsif($tag eq 'code_end') {
-      $ret .= '
' if !$nobreak; + $ret .= $opt{text} || $opt{inline} ? e '`' : '
'; $rmnewline = 1; $incode = 0; + } elsif($tag eq 'spoiler_start') { + $inspoil = 1; + $ret .= $opt{delspoil} || $opt{keepspoil} ? '' + : $opt{replacespoil} ? '<hidden by spoiler settings>' + : ''; + } elsif($tag eq 'spoiler_end') { + $inspoil = 0; + $ret .= $opt{delspoil} || $opt{keepspoil} || $opt{replacespoil} ? '' : ''; + } elsif($tag eq 'url_start') { - $ret .= sprintf '', xml_escape($arg[0]); + $ret .= $opt{text} ? '' : sprintf '', xml_escape($arg[0]); } elsif($tag eq 'url_end') { - $ret .= ''; + $ret .= $opt{text} ? '' : ''; } elsif($tag eq 'link') { - $ret .= sprintf '%s', xml_escape($raw), $e->('link'); - - } elsif($tag eq 'dblink') { - (my $link = $raw) =~ s/^d(\d+)\.(\d+)\.(\d+)$/d$1#$2.$3/; - $ret .= sprintf '%s', $link, $e->($raw); + $ret .= $opt{text} ? e $raw : sprintf '%s', xml_escape($raw), e 'link'; } - !$maxlength || $length < $maxlength; - }; - $ret; -} - - -# Convert bbcode into plain text, stripping all tags and spoilers. [url] tags -# only display the title. -sub bb2text { - my $input = shift; - - my $inspoil = 0; - my $ret = ''; - parse $input, sub { - my($raw, $tag, @arg) = @_; - if($tag eq 'spoiler_start') { - $inspoil = 1; - } elsif($tag eq 'spoiler_end') { - $inspoil = 0; - } else { - $ret .= $raw if !$inspoil && $tag !~ /_(start|end)$/; - } - 1; + !$opt{maxlength} || $length < $opt{maxlength}; }; $ret; } diff --git a/lib/VNDB/Func.pm b/lib/VNDB/Func.pm index 2a169552..2fcd5b54 100644 --- a/lib/VNDB/Func.pm +++ b/lib/VNDB/Func.pm @@ -10,7 +10,7 @@ use JSON::XS; use VNDBUtil; use VNDB::Types; use VNDB::BBCode; -our @EXPORT = (@VNDBUtil::EXPORT, 'bb2html', 'bb2text', qw| +our @EXPORT = (@VNDBUtil::EXPORT, 'bb_format', qw| clearfloat cssicon minage fil_parse fil_serialize parenttags childtags charspoil imgpath imgurl fmtvote fmtmedia fmtvnlen fmtage fmtdatestr fmtdate fmtrating fmtspoil diff --git a/lib/VNDB/Handler/Misc.pm b/lib/VNDB/Handler/Misc.pm index 906c7c78..b7e1620d 100644 --- a/lib/VNDB/Handler/Misc.pm +++ b/lib/VNDB/Handler/Misc.pm @@ -105,7 +105,7 @@ sub homepage { a href => "/$_->{id}", $_->{title}; end; p; - lit bb2html $post->{msg}, 150, 0, 1; + lit bb_format $post->{msg}, maxlength => 150, inline => 1; end; } end 'td'; diff --git a/lib/VNDB/Handler/Tags.pm b/lib/VNDB/Handler/Tags.pm index c44529cf..55bf99db 100644 --- a/lib/VNDB/Handler/Tags.pm +++ b/lib/VNDB/Handler/Tags.pm @@ -83,7 +83,7 @@ sub tagpage { if($t->{description}) { p class => 'description'; - lit bb2html $t->{description}; + lit bb_format $t->{description}; end; } if(!$t->{applicable} || !$t->{searchable}) { diff --git a/lib/VNDB/Handler/Traits.pm b/lib/VNDB/Handler/Traits.pm index f9802cff..e69b673e 100644 --- a/lib/VNDB/Handler/Traits.pm +++ b/lib/VNDB/Handler/Traits.pm @@ -64,7 +64,7 @@ sub traitpage { if($t->{description}) { p class => 'description'; - lit bb2html $t->{description}; + lit bb_format $t->{description}; end; } if(!$t->{applicable} || !$t->{searchable}) { diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm index 1e11aa7b..1198a421 100644 --- a/lib/VNDB/Handler/VNPage.pm +++ b/lib/VNDB/Handler/VNPage.pm @@ -149,7 +149,7 @@ my @rel_cols = ( default => 1, what => 'extended', has_data => sub { !!$_[0]{notes} }, - draw => sub { lit bb2html $_[0]{notes} }, + draw => sub { lit bb_format $_[0]{notes} }, } ); diff --git a/lib/VNWeb/Chars/Page.pm b/lib/VNWeb/Chars/Page.pm index 2accc94b..b131b7e9 100644 --- a/lib/VNWeb/Chars/Page.pm +++ b/lib/VNWeb/Chars/Page.pm @@ -213,7 +213,7 @@ sub chartable_ { tr_ class => 'nostripe', sub { td_ colspan => 2, class => 'chardesc', sub { h2_ 'Description'; - p_ sub { lit_ bb2html $c->{desc}, 0, $view->{spoilers} == 2 ? 3 : 2 }; + p_ sub { lit_ bb_format $c->{desc}, delspoil => $view->{spoilers} != 2, keepspoil => $view->{spoilers} == 2 }; }; } if $c->{desc}; }; @@ -251,7 +251,7 @@ TUWF::get qr{/$RE{crev}} => sub { framework_ title => $c->{name}, index => !tuwf->capture('rev'), type => 'c', dbobj => $c, hiddenmsg => 1, og => { - description => bb2text($c->{desc}), + description => bb_format($c->{desc}, text => 1), image => $c->{image} && $c->{image}{votecount} && !$c->{image}{sexual} && !$c->{image}{violence} ? tuwf->imgurl($c->{image}{id}) : undef, }, sub { diff --git a/lib/VNWeb/Discussions/Thread.pm b/lib/VNWeb/Discussions/Thread.pm index bafe5449..80b7e544 100644 --- a/lib/VNWeb/Discussions/Thread.pm +++ b/lib/VNWeb/Discussions/Thread.pm @@ -70,7 +70,7 @@ elm_api DiscussionsReply => $REPLY_OUT, $REPLY_IN, sub { sub metabox_ { my($t) = @_; div_ class => 'mainbox', sub { - h1_ sub { lit_ bb2html $t->{title} }; + h1_ sub { lit_ bb_format $t->{title}, idonly => 1 }; h2_ 'Hidden' if $t->{hidden}; h2_ 'Private' if $t->{private}; h2_ 'Locked' if $t->{locked}; @@ -125,7 +125,7 @@ sub posts_ { if($_->{hidden}) { i_ class => 'deleted', 'Post deleted.'; } else { - lit_ bb2html $_->{msg}; + lit_ bb_format $_->{msg}; i_ class => 'lastmod', 'Last modified on '.fmtdate($_->{edited}, 'full') if $_->{edited}; } }; diff --git a/lib/VNWeb/Discussions/UPosts.pm b/lib/VNWeb/Discussions/UPosts.pm index 46bb0977..d3bfa95c 100644 --- a/lib/VNWeb/Discussions/UPosts.pm +++ b/lib/VNWeb/Discussions/UPosts.pm @@ -24,7 +24,7 @@ sub listing_ { td_ class => 'tc3', fmtdate $_->{date}; td_ class => 'tc4', sub { a_ href => $url, $_->{title}; - b_ class => 'grayedout', sub { lit_ bb2html $_->{msg}, 150, 0, 1 }; + b_ class => 'grayedout', sub { lit_ bb_format $_->{msg}, maxlength => 150, inline => 1 }; }; } for @$list; } diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm index 1a4dc617..772f3ebc 100644 --- a/lib/VNWeb/HTML.pm +++ b/lib/VNWeb/HTML.pm @@ -418,7 +418,7 @@ sub _hidden_msg_ { txt_ ' if you believe that this entry should be restored.'; br_; br_; - lit_ bb2html $msg; + lit_ bb_format $msg; } } }; @@ -614,7 +614,7 @@ sub _revision_cmp_ { b_ "Edit summary for revision $new->{chrev}"; br_; br_; - lit_ bb2html $new->{rev_comments}||'-'; + lit_ bb_format $new->{rev_comments}||'-'; }; }; }; @@ -677,7 +677,7 @@ sub revision_ { br_; b_ 'Edit summary'; br_; br_; - lit_ bb2html $new->{rev_comments}||'-'; + lit_ bb_format $new->{rev_comments}||'-'; } if !$old; _revision_cmp_ $type, $old, $new, @fields if $old; diff --git a/lib/VNWeb/Misc/BBCode.pm b/lib/VNWeb/Misc/BBCode.pm index 5d6f2e0b..2c41b6da 100644 --- a/lib/VNWeb/Misc/BBCode.pm +++ b/lib/VNWeb/Misc/BBCode.pm @@ -5,7 +5,7 @@ use VNWeb::Prelude; elm_api BBCode => undef, { content => { required => 0, default => '' } }, sub { - elm_Content bb2html bb_subst_links shift->{content}; + elm_Content bb_format bb_subst_links shift->{content}; }; 1; diff --git a/lib/VNWeb/Misc/History.pm b/lib/VNWeb/Misc/History.pm index 0959eb91..927afa98 100644 --- a/lib/VNWeb/Misc/History.pm +++ b/lib/VNWeb/Misc/History.pm @@ -80,7 +80,7 @@ sub tablebox_ { td_ class => 'tc3', sub { user_ $i }; td_ class => 'tc4', sub { a_ href => $revurl, title => $i->{original}, shorten $i->{title}, 80; - b_ class => 'grayedout', sub { lit_ bb2html $i->{comments}, 150, 0, 1 }; + b_ class => 'grayedout', sub { lit_ bb_format $i->{comments}, maxlength => 150, inline => 1 }; }; } for @$lst; }; diff --git a/lib/VNWeb/Misc/Reports.pm b/lib/VNWeb/Misc/Reports.pm index b932cd31..c275efa9 100644 --- a/lib/VNWeb/Misc/Reports.pm +++ b/lib/VNWeb/Misc/Reports.pm @@ -130,7 +130,7 @@ sub report_ { lit_ $r->{title} || '[deleted]'; br_; txt_ $r->{reason}; - div_ class => 'quote', sub { lit_ bb2html $r->{message} } if $r->{message}; + div_ class => 'quote', sub { lit_ bb_format $r->{message} } if $r->{message}; }; td_ style => 'width: 300px', sub { form_ method => 'post', action => '/report/edit', sub { @@ -145,7 +145,7 @@ sub report_ { }; }; td_ sub { - lit_ bb2html $r->{log}; + lit_ bb_format $r->{log}; }; } diff --git a/lib/VNWeb/Producers/Page.pm b/lib/VNWeb/Producers/Page.pm index 802218d6..eac6f3e4 100644 --- a/lib/VNWeb/Producers/Page.pm +++ b/lib/VNWeb/Producers/Page.pm @@ -59,7 +59,7 @@ sub info_ { }, grep $rel{$_}, keys %PRODUCER_RELATION; } if $p->{relations}->@*; - p_ class => 'description', sub { lit_ bb2html $p->{desc} } if length $p->{desc}; + p_ class => 'description', sub { lit_ bb_format $p->{desc} } if length $p->{desc}; } @@ -155,7 +155,7 @@ TUWF::get qr{/$RE{prev}(?:/(?vn|rel))?}, sub { framework_ title => $p->{name}, index => !tuwf->capture('rev'), type => 'p', dbobj => $p, hiddenmsg => 1, og => { title => $p->{name}, - description => bb2text($p->{desc}), + description => bb_format($p->{desc}, text => 1), }, sub { rev_ $p if tuwf->capture('rev'); diff --git a/lib/VNWeb/Releases/Lib.pm b/lib/VNWeb/Releases/Lib.pm index 87f9c401..4aad7b50 100644 --- a/lib/VNWeb/Releases/Lib.pm +++ b/lib/VNWeb/Releases/Lib.pm @@ -92,7 +92,7 @@ sub release_row_ { } icon_ $MEDIUM{ $r->{media}[0]{medium} }{icon}, join ', ', map fmtmedia($_->{medium}, $_->{qty}), $r->{media}->@* if $r->{media}->@*; icon_ 'uncensor', 'Uncensored' if $r->{uncensored}; - icon_ 'notes', bb2text $r->{notes} if $r->{notes}; + icon_ 'notes', bb_format $r->{notes}, text => 1 if $r->{notes}; } tr_ sub { diff --git a/lib/VNWeb/Releases/Page.pm b/lib/VNWeb/Releases/Page.pm index d7cbb745..e60d84b6 100644 --- a/lib/VNWeb/Releases/Page.pm +++ b/lib/VNWeb/Releases/Page.pm @@ -212,7 +212,7 @@ TUWF::get qr{/$RE{rrev}} => sub { framework_ title => $r->{title}, index => !tuwf->capture('rev'), type => 'r', dbobj => $r, hiddenmsg => 1, og => { - description => bb2text $r->{notes} + description => bb_format $r->{notes}, text => 1 }, sub { _rev_ $r if tuwf->capture('rev'); @@ -221,7 +221,7 @@ TUWF::get qr{/$RE{rrev}} => sub { h1_ sub { txt_ $r->{title}; debug_ $r }; h2_ class => 'alttitle', lang_attr($r->{lang}), $r->{original} if length $r->{original}; _infotable_ $r; - p_ class => 'description', sub { lit_ bb2html $r->{notes} } if $r->{notes}; + p_ class => 'description', sub { lit_ bb_format $r->{notes} } if $r->{notes}; }; }; }; diff --git a/lib/VNWeb/Reviews/Page.pm b/lib/VNWeb/Reviews/Page.pm index 36ed8ee2..55847bf6 100644 --- a/lib/VNWeb/Reviews/Page.pm +++ b/lib/VNWeb/Reviews/Page.pm @@ -60,7 +60,7 @@ sub review_ { } if $w->{spoiler}; tr_ @spoil, sub { td_ 'Review'; - td_ sub { lit_ bb2html $w->{text} } + td_ sub { lit_ bb_format $w->{text} } }; tr_ sub { td_ ''; diff --git a/lib/VNWeb/Reviews/VNTab.pm b/lib/VNWeb/Reviews/VNTab.pm index bd1ae870..9b5427d1 100644 --- a/lib/VNWeb/Reviews/VNTab.pm +++ b/lib/VNWeb/Reviews/VNTab.pm @@ -40,7 +40,7 @@ sub reviews_ { a_ href => "/report/$r->{id}", 'report'; txt_ '>'; }; - my $html = bb2html $r->{text}, $mini ? undef : 700; + my $html = bb_format $r->{text}, maxlength => $mini ? undef : 700; $html .= '...' if !$mini; if($r->{spoiler}) { label_ class => 'review_spoil', sub { diff --git a/lib/VNWeb/Staff/Page.pm b/lib/VNWeb/Staff/Page.pm index 72227559..8f6e0897 100644 --- a/lib/VNWeb/Staff/Page.pm +++ b/lib/VNWeb/Staff/Page.pm @@ -171,7 +171,7 @@ TUWF::get qr{/$RE{srev}} => sub { framework_ title => $main->{name}, index => !tuwf->capture('rev'), type => 's', dbobj => $s, hiddenmsg => 1, og => { - description => bb2text $s->{desc} + description => bb_format $s->{desc}, text => 1 }, sub { _rev_ $s if tuwf->capture('rev'); @@ -180,7 +180,7 @@ TUWF::get qr{/$RE{srev}} => sub { h1_ sub { txt_ $main->{name}; debug_ $s }; h2_ class => 'alttitle', lang => $s->{lang}, $main->{original} if $main->{original}; _infotable_ $main, $s; - p_ class => 'description', sub { lit_ bb2html $s->{desc} }; + p_ class => 'description', sub { lit_ bb_format $s->{desc} }; }; _roles_ $s; diff --git a/lib/VNWeb/VN/Page.pm b/lib/VNWeb/VN/Page.pm index 0692a076..683e3af0 100644 --- a/lib/VNWeb/VN/Page.pm +++ b/lib/VNWeb/VN/Page.pm @@ -49,7 +49,7 @@ sub enrich_item { sub og { my($v) = @_; +{ - description => bb2text($v->{desc}), + description => bb_format($v->{desc}, text => 1), image => $v->{image} && !$v->{image}{sexual} && !$v->{image}{violence} ? tuwf->imgurl($v->{image}{id}) : [map $_->{scr}{sexual}||$_->{scr}{violence}?():(tuwf->imgurl($_->{scr}{id})), $v->{screenshots}->@*]->[0] } @@ -375,7 +375,7 @@ sub infobox_ { tr_ class => 'nostripe', sub { td_ class => 'vndesc', colspan => 2, sub { h2_ 'Description'; - p_ sub { lit_ $v->{desc} ? bb2html $v->{desc} : '-' }; + p_ sub { lit_ $v->{desc} ? bb_format $v->{desc} : '-' }; } } } -- cgit v1.2.3