+ $$cb->write("<html><head><title>$s</title>".
+ qq{<link\nrel=alternate\ntitle="Atom feed"\n} .
+ qq!href="../t.atom"\ntype="application/atom+xml"/>! .
+ PublicInbox::Hval::STYLE .
+ "</head><body>");
+}
+
+sub pre_anchor_entry {
+ my ($seen, $mime) = @_;
+ my $id = anchor_for(mid_mime($mime));
+ $seen->{$id} = "#$id"; # save the anchor for children, later
+}
+
+sub ghost_parent {
+ my ($upfx, $mid) = @_;
+ # 'subject dummy' is used internally by Mail::Thread
+ return '[no common parent]' if ($mid eq 'subject dummy');
+
+ $mid = PublicInbox::Hval->new_msgid($mid);
+ my $href = $mid->as_href;
+ my $html = $mid->as_html;
+ qq{[parent not found: <<a\nhref="$upfx$href/">$html</a>>]};
+}
+
+sub thread_adj_level {
+ my ($fh, $state, $level) = @_;
+
+ my $max = $state->{cur_level};
+ if ($level <= 0) {
+ return '' if $max == 0; # flat output
+
+ # reset existing lists
+ my $x = $max > 1 ? ('</ul></li>' x ($max - 1)) : '';
+ $fh->write($x . '</ul>');
+ $state->{cur_level} = 0;
+ return '';
+ }
+ if ($level == $max) { # continue existing list
+ $fh->write('<li>');
+ } elsif ($level < $max) {
+ my $x = $max > 1 ? ('</ul></li>' x ($max - $level)) : '';
+ $fh->write($x .= '<li>');
+ $state->{cur_level} = $level;
+ } else { # ($level > $max) # start a new level
+ $state->{cur_level} = $level;
+ $fh->write(($max ? '<li>' : '') . '<ul><li>');
+ }
+ '</li>';
+}
+
+sub ghost_flush {
+ my ($fh, $state, $upfx, $mid, $level) = @_;
+
+ my $end = thread_adj_level($fh, $state, $level);
+ $fh->write('<pre>'. ghost_parent($upfx, $mid) . '</pre>' . $end);
+}
+
+sub __thread_entry {
+ my ($cb, $git, $state, $mime, $level) = @_;
+
+ # lazy load the full message from mini_mime:
+ $mime = eval {
+ my $path = mid2path(mid_clean(mid_mime($mime)));
+ Email::MIME->new($git->cat_file('HEAD:'.$path));
+ } or return;
+
+ if ($state->{anchor_idx} == 0) {
+ thread_html_head($cb, $mime, $state, $level);
+ }
+ my $fh = $$cb;
+ if (my $ghost = delete $state->{ghost}) {
+ # n.b. ghost messages may only be parents, not children
+ foreach my $g (@$ghost) {
+ ghost_flush($fh, $state, '../../', @$g);
+ }
+ }
+ my $end = thread_adj_level($fh, $state, $level);
+ index_entry($fh, $mime, $level, $state);
+ $fh->write($end) if $end;
+
+ 1;
+}
+
+sub indent_for {
+ my ($level) = @_;
+ INDENT x ($level - 1);
+}
+
+sub __ghost_prepare {
+ my ($state, $node, $level) = @_;
+ my $ghost = $state->{ghost} ||= [];
+ push @$ghost, [ $node->messageid, $level ];