### ----------------------------------------------------
### -- Rakuast::RakuDoc::Render
### -- Licenses: Artistic-2.0
### -- Authors: Richard Hainsworth
### -- File: lib/RakuDoc/Render.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 4
-head
                    !! self.index-id(:contents(textify-markup($ast)));
                my %ref = %( :$target, :$is-in-heading, :$place );
                %ref<place> = PCell.new(:id($target),:$.register )
                    if $is-in-heading; # the title of the header is not known until later
                if $ast.meta and $meta { # this will be true if the MarkupMeta grammar parsed
                    if $meta.made<type> eq 'plain-string' {
                        $.merge-index( $prs.index, $.add-index( $meta.made<value>, %ref ))
                    }
                    else {
                        $.merge-index( $prs.index, $.add-index( $_, %ref )) for $meta.made<value>.list
                    }
                }
                elsif $ast.meta { # this is true if MarkupMeta grammar failed on an existing $ast.meta
                    $prs.index{$contents} = %( :refs( [] ), :sub-index( {} ) ) unless $prs.index{$contents}:exists;
                    $prs.index{$contents}<refs>.push: %ref;
                    $prs.warnings.push('Ignoring content of X<> after | ｢'
                        ~ $ast.meta.Str ~ '｣'
                        ~ " in block ｢$context｣ with heading ｢$place｣.")
                        if %config<error>
                }
                else { # no meta
                    $prs.index{$contents} = %( :refs( [] ), :sub-index( {} ) ) unless $prs.index{$contents}:exists;
                    $prs.index{$contents}<refs>.push: %ref
                }
                my $rv = %!templates{"markup-$letter"}(
                    %( :$contents, :$meta, :$target, :$place, :$is-in-heading, %config )
                ).Str;
                $prs.body ~= $rv;
            }

            ## Technically only meta data, but just contents

            # Z< METADATA = COMMENT >
            # Comment zero-width  (contents never rendered)
            when 'Z' {
                $prs.body ~= '';
            }

            ## Undefined and reserved, so generate warnings
            # do not go through templates as these cannot be redefined
            when any(<G Q W Y>) {
                $prs.body ~= %!templates{"markup-bad"}( %( :contents($ast.DEPARSE), ) );
                $prs.warnings.push(
                    "｢$letter｣ is not defined, but is reserved for future use"
                        ~ " in block ｢$context｣ with heading ｢$place｣.")
                    if %config<error>
            }
            when (.uniprop ~~ / Lu / and %!templates{ "markup-$letter" }:exists) {
                my $contents = self.markup-contents($ast);
                $prs.body ~= %!templates{ "markup-$letter" }(
                    %( :$contents, %config )
                );
            }
            when (.uniprop ~~ / Lu /) {
                $prs.body ~= %!templates{"markup-bad"}( %( :contents($ast.DEPARSE), ) );
                $prs.warnings.push(
                    "｢$letter｣ does not have a template, but could be a custom code"
                        ~ " in block ｢$context｣ with heading ｢$place｣.")
                    if %config<error>
            }
            default {
                $prs.body ~= %!templates{"markup-bad"}( %( :contents($ast.DEPARSE), ) );
                $prs.warnings.push("｢$letter｣ may not be a markup code"
                        ~ " in block ｢$context｣ with heading ｢$place｣.")
                    if %config<error>
            }
        }
    }
    #| This block is created by the parser when a text has embedded markup
    #| Also ordinary strings in an extended block are coerced into one
    #| Sometimes, eg for a table cell, the paragraph should not be
    #| ended with a newline.
    multi method handle(RakuAST::Doc::Paragraph:D $ast ) {
        if $!scoped-data.verbatim {
            $ast.atoms.map({ $.handle($_) });
            return
        }
        my $inline = $!scoped-data.last-starter eq 'table';
        my %config = $.merged-config($, 'para' );
        my $rem = $.complete-item-list ~ $.complete-defn-list
        ~ $.complete-numitem-list ~ $.complete-numdefn-list;
        do {
            my ProcessedState $*prs .= new;
            for $ast.atoms { $.handle($_) }
            my $prs := $*prs;
            my PStr $contents = $prs.body;
            # each para should have a target, generate a SHA if no id given
            my $target = %config<id> // $.para-target($contents);
            my $is-in-head = $!scoped-data.in-head.so;
            $prs.body .= new( $rem );
            my $rv = %!templates<para>(
                %( :$contents, :$target, :$inline, :$is-in-head, %config)
            );
            # deal with possible inline definitions
            if $prs.inline-defns.elems {
                for $prs.inline-defns.list -> $term {
                    $prs.warnings.push(
                        "Definition ｢$term｣ has been redefined as an inline"
                        ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣.")
                        if $prs.definitions{ $term }:exists && %config<error>;
                    $prs.definitions{ $term } = $rv, $target;
                    $!register.add-payload(:payload($rv), :id($term));
                    $!register.add-payload(:payload($target), :id($term ~ '_target'))
                }
                $prs.inline-defns = ()
            }
            $prs.body ~= $rv;
            CALLERS::<$*prs> += $prs;
       }
    }
    multi method handle(RakuAST::Doc::LegacyRow:D $ast) {
        $*prs.warnings.push: qq:to/WARN/;
            The line ｢{ $ast.Str }｣ should not appear outside the scope of a =table
            WARN
    }
    multi method handle(Cool:D $ast) {
        self.handle($ast.WHICH.Str)
    }

    # helper methods for markup
    #| similar to merge-index in Processed, but simpler because less generic
    multi method merge-index( %p, %q ) {
        for %q.keys -> $k {
            if %p{$k}:exists {
                %p{$k}<refs>.append: %q{$k}<refs>.Slip;
                $.merge-index( %p{$k}<sub-index>, %q{$k}<sub-index> )
            }
            else {
                %p{$k} = %q{$k};
            }
        }
    }
    multi method add-index( Str:D $r, $ref ) {
        my @refs;
        @refs.push: $ref;
        %( $r => %( :@refs, sub-index => %( ) ) )
    }
    #| adding in a string means only the last element in the string has the reference
    #| the earlier ones only have references if given by other calls to index
    multi method add-index( @r, $ref --> Hash ) {
        return $.add-index( @r[0].Str, $ref ) if @r.elems == 1;
        return %() unless +@r;
        my @refs;
        my %h = %( :@refs, sub-index => %( ) );
        $.merge-index( %h<sub-index>, $.add-index( @r[ 1 .. *-1 ] , $ref ) ) if @r[1 .. *-1].elems.so;
        %( @r[0] => %h.clone )
    }

    # gen-XX methods take an $ast, process the contents, based on a template,
    # and add the string to a structure, typically, not always, to $*prs.body

    #| generic code for next, para, code, input, output blocks
    #| No ToC content is added unless overridden by toc/caption/headlevel
    method gen-paraish( $ast, $template, $parify ) {
        my %config = $.merged-config( $ast, $template);
        if %config<toc> {
            my $caption = %config<caption> // $template.tc;
            my $level = %config<headlevel> // 1;
            $level = 1 if $level < 1;
            my $numeration = '';
            my $target = %config<id> // $.name-id($caption);
            $*prs.toc.push: %( :$caption, :$level, :$numeration, :$target );
            %config<target> = $target;
        }
        my PStr $contents .= new;
        $!scoped-data.start-scope(:starter($template), :verbatim )
            if $template ~~ any(<code implicit-code input output>);
        if $template eq 'code' {
            if %config<allow>:exists {
                %config<in_code_context> = True;
                my %*ALLOW = %config<allow>
                    .grep({ any(@format-codes) })
                    .map({ $_ => True }).hash;
                $contents ~= $.contents($ast, $parify);
            }
            else {
                $contents ~= $ast.paragraphs.map( *.Str ).join
            }
        }
        else {
            $contents ~= $.contents($ast, $parify);
        }
        my Bool $is-in-heading = $!scoped-data.in-head.so;
        $contents ~= $.complete-item-list;
        $contents ~= $.complete-defn-list;
        $contents ~= $.complete-numitem-list;
        $contents ~= $.complete-numdefn-list;
        $*prs.body ~= %!templates{ $template }(
            %( :$contents, :$is-in-heading, %config)
        );
        $!scoped-data.end-scope if $template ~~ any( <code implicit-code input output> )
    }
    #| A header adds contents at given level to ToC, unless overridden by toc/headlevel/caption
    #| These can be set by a config directive
    #| The id option may be used to create a target
    #| An automatic target is also created from the contents
    method gen-headish($ast, $parify, :$template = 'head', :$numerate = False ) {
        # stringify ast for internal use
        # set up data for embedded markup in header, eg., X
        my $title = textify-head($ast);
        my $target = $.name-id($title);
        $!scoped-data.start-scope(:starter('head'), :$title ) if $parify;
        # must set in-head in case of other inner scopes.
        $!scoped-data.in-head = $target;
        my $contents = $.contents($ast, $parify).strip.trim.Str;
        my $prs := $*prs;
        my $level = $ast.level > 1 ?? $ast.level !! 1;
        my %config = $.merged-config($ast,($template ~ $level) );
        my $caption = %config<caption>:delete;
        $caption = $contents without $caption;
        # allow internal X<> to have the final rendered title
        $!register.add-payload(:payload($caption), :id( $target ) );
        # set the last title for paragraphs following the title
        $!scoped-data.last-title( $caption );
        my $id = %config<id>:delete ;
        with $id {
            if self.is-target-unique( $_ ) {
                $id = self.register-target( $_ );
            }
            else {
                $prs.warnings.push("Attempt to register already existing id ｢$_｣ as new target in heading ｢$contents｣")
                if %config<error>
            }
        }
        else { $id = '' }
        # level is over-ridden if headlevel is set, eg =for head2 :headlevel(3)
        $level = %config<headlevel> if %config<headlevel>:exists;
        $level = 1 if $level < 1;
        # numeration is attached to contents first
        my $numeration = '';
        if $numerate {
            $numeration ~= PCell.new( :$!register, :id('heading_' ~ $target ) );
            $prs.head-numbering.push: ['heading_' ~ $target, $level ];
        }
        my $toc = %config<toc>:delete // True;
        # attach numeration to caption and contents separately, allowing template
        # developer to add numeration to caption if wanted by changing the template
        $prs.toc.push(
            { :$caption, :$target, :$level, :$numeration }
        ) if $toc;
        $prs.body ~= %!templates{ $template }(
            %( :$numeration, :$level, :$target, :$contents, :$toc, :$caption, :$id, %config )
        );
        $!scoped-data.in-head = '';
        $!scoped-data.end-scope if $parify;
    }
    #| Formula at level 1 is added to ToC unless overriden by toc/headlevel/caption
    #| Content is passed verbatim to template as formula
    #| An alt text is also generated
    method gen-formula($ast) {
        my %config = $.merged-config( $ast, 'formula' );
        my $formula = $.contents( $ast, False ); # do not treat strings paragraphs
        my $raw = $ast.paragraphs.Str.join; # also create a raw version for other renderers
        my $prs := $*prs;
        my $alt = %config<alt>:delete // 'Formula cannot be rendered';
        my $caption = %config<caption>:delete // 'Formula';
        my $target = $.name-id($alt);
        my $id = %config<id>:delete;
        my $numeration = ''; # TODO allow for TABLE and FORMULA numeration
        with $id {
            if self.is-target-unique( $_ ) {
                $id = self.register-target( $_ );
            }
            else {
                $prs.warnings.push("Attempt to register already existing id ｢$_｣ as new target in heading ｢$alt｣")
                if %config<error>
            }
        }
        # level is over-ridden if headlevel is set, eg =for head2 :headlevel(3)
        my $level = %config<headlevel> // 1;
        $level = 1 if $level < 1;
        my $toc = %config<toc>:delete // True;
        # attach numeration to caption and contents separately, allowing template
        # developer to add numeration to caption if wanted by changing the template
        $prs.toc.push(
            { :$caption, :$target, :$level, :$numeration }
        ) if $toc;
        $prs.body ~= %!templates<formula>(%(:$raw, :$formula, :$alt, :$target, :$caption, :$level, :$numeration, :$id, %config ) )
    }
    #| generates a single item and adds it to the item structure
    #| nothing is added to the .body string
    #| bullet strategy can be left to template, with bullet in %config
    method gen-item($ast, $parify) {
        my $level = $ast.level > 1 ?? $ast.level !! 1;
        my $contents = $.contents($ast, $parify);
        my %config = $.merged-config($ast, 'item' ~ $level );
        $*prs.items.push: %!templates<item>(
            %( :$level, :$contents, %config )
        )
    }
    #| generates a single numitem and adds it to the numitem structure
    #| nothing is added to the .body string
    method gen-numitem($ast, $parify) {
        my $level = $ast.level > 1 ?? $ast.level !! 1;
        my $contents = $.contents($ast, $parify);
        my %config = $.merged-config($ast, 'numitem' ~ $level );
        $!scoped-data.item-reset unless ($*prs.numitems.elems or %config<continued>);
        my $numeration = $!scoped-data.item-inc($level);
        $*prs.numitems.push: %!templates<numitem>(
            %( :$level, :$contents, :$numeration, %config )
        )
    }
    #| generates a single definition and adds it to the defn structure
    #| unlike item, a defn:
    #| - list has a flat hierarchy
    #| - can be created by a markup code
    #| - needs a target for links, and text for popup
    #| - is PCell-stored allowing for defn to be redefined
    #| like items nothing is added to the .body string until next non-defn
    method gen-defn($ast, :$numerate = False) {
        my $term;
        my $defn-expansion;
        my %config = $.merged-config($ast, $numerate ?? 'numdefn' !! 'defn');
        if $ast.paragraphs.elems == 2 {
            $term = $ast.paragraphs[0].Str.trim; # the term may not contain embedded code
            $defn-expansion = $ast.paragraphs[1];
        }
        else {
            my $string = $ast.Str;
            $*prs.body ~= $string;
            $*prs.warnings.push(
                "Invalid definition: ｢$string｣"
                ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣.")
                if %config<error>;
            return
        }
        my $target = $.name-id("defn_$term");
        my $contents;
        # generate contents from second str/paragraph
        do {
            my ProcessedState $*prs .= new;
            $.handle( $defn-expansion );
            my $prs := $*prs;
            $contents = $prs.body;
            # keep most of state, initialise body
            $prs.body .= new;
            CALLERS::<$*prs> += $prs;
        }
        my $prs := $*prs;
        if $numerate {
            $!scoped-data.defn-reset unless ($prs.numdefns.elems or %config<continued>);
            my $numeration = $!scoped-data.defn-inc;
            $defn-expansion = %!templates<numdefn>(
                %( :$term, :$target, :$contents, :$numeration, %config )
            );
            $prs.numdefns.push: $defn-expansion
        }
        else {
            $defn-expansion = %!templates<defn>(
                %( :$term, :$target, :$contents, %config )
            );
            $prs.defns.push: $defn-expansion; # for the defn list to be render
        }
        $prs.warnings.push(
            "Definition ｢$term｣ has been redefined"
            ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣.")
            if $prs.definitions{ $term }:exists and %config<error>;
        $prs.definitions{ $term } = $defn-expansion, $target;
        # define for previously referenced
        $!register.add-payload(:payload($defn-expansion), :id($term));
        $!register.add-payload(:payload($target), :id($term ~ '_target'))
    }
    #| A place block adds Place at level 1 to ToC unless toc/headlevel/caption set
    #| The contents of Place is a URI that is generated and then rendered with place template
    method gen-place($ast) {
        my %config = $.merged-config( $ast, 'place');
        my $uri = %config<uri>:delete;
        unless %config<caption>:exists {
            if $uri ~~ / 'semantic:' (.+) / {
                %config<caption> = ~$0
            }
            else { %config<caption> = 'Placement' }
        }
        my $caption = %config<caption>;
        my $level = %config<headlevel> // 1;
        $!scoped-data.last-title($caption);
        my $target = %config<target> = $.name-id($caption);
        with %config<id> {
            if self.is-target-unique( $_ ) {
                %config<id> = self.register-target( $_ )
            }
            else {
                $*prs.warnings.push("Attempt to register already existing id ｢$_｣ as new target in heading ｢$caption｣")
                if %config<error>
            }
        }
        my $toc = %config<toc> // True;
        $*prs.toc.push(
            { :$caption, :$target, :$level }
        ) if $toc;
        $.make-placement(:$uri, :%config, :template<place>);
    }
    method make-placement( :$uri, :%config, :$template ) {
        my Bool $keep-format = False;
        # defaults when no schema is explicit
        my $schema = 'file';
        my $uri-body = $uri;
        my $contents;
        #defaults to text type of contents, could be blob from https
        %config<content-type> = 'text';
        my $prs := $*prs;
        if $uri ~~ / ^ $<sch> = (\w+) ':' $<body> = (.+) $ / {
            $schema = $/<sch>.Str;
            $uri-body = $/<body>.Str
        }
        given $schema {
            when 'toc' {
                $contents = PCell.new( :$!register, :id("toc_$uri-body"), :spec($uri-body) );
                $keep-format = True;
            }
            when 'index' {
                $contents = PCell.new( :$!register, :id("index_$uri-body"), :spec($uri-body) );
                $keep-format = True;
            }
            when 'semantic' {
                $keep-format = True;
                $contents =  PCell.new( :$!register, :id( "semantic_$uri-body" ), :spec($uri-body) );
            }
            when 'http' | 'https' {
                my LibCurl::Easy $curl .= new(:URL($uri), :followlocation, :failonerror );
                try {
                    $curl.perform;
                    %config<content-type> = $curl.Content-Type;
                    if %config<content-type>.contains('text') {
                        $contents = $curl.perform.content;
                        %config<html> = so $contents ~~ / '<html' .+ '</html>'/;
                        $contents = ~$/ if %config<html>; # strip off any chars before & after the <html> container if it exists
                    }
                    else {
                        $contents = $curl.perform.buf;
                    }
                    CATCH {
                        default {
                            my $error = "Link ｢$uri｣ caused LibCurl Exception, response code ｢{ $curl.response-code }｣ with error ｢{ $curl.error }｣";
                            $contents = %config<fallback> // $error;
                            $prs.warnings.push($error) if %config<error>
                        }
                    }
                }
            }
            when 'file' {
                my URI $f-uri .= new($uri);
                if $f-uri.path.Str.IO ~~ :e & :f {
                    $contents = $f-uri.path.Str.IO.slurp;
                }
                else {
                    my $error = "No file found at ｢$f-uri｣";
                    $contents = %config<fallback> // $error;
                    $prs.warnings.push($error) if %config<error>
                }
            }
            when 'defn' {
                # get definition from Processed state, or make a PCell
                my %definitions = $prs.definitions;
                $contents = $uri-body;
                if %definitions{ $uri-body }:exists {
                    %config<defn-expansion> = %definitions{ $uri-body }[0];
                    %config<defn-target> = %definitions{ $uri-body }[1]
                }
                else {
                    %config<defn-expansion> = PCell.new( :$!register, :id( $uri-body ));
                    %config<defn-target> = PCell.new( :$!register, :id( $uri-body ~ '_target' ));
                }
            }
            default {
                    $contents = %config<fallback> // "See $uri";
                    $prs.warnings.push(
                        "The schema ｢$schema｣ is not implemented. Full link was ｢$uri｣"
                        ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣.") if %config<error>
            }
        }
        # trap contents that have RakuDoc, could be from https, or file and attempt to process it.
        if %config<content-type>.contains('text') && $contents ~~ /^ '=begin rakudoc' / {
             do {
                my ProcessedState $*prs .= new;
                $contents.AST.rakudoc.map( { $.handle( $_ ) } );
                my $prs := $*prs;
                $contents = $prs.body.trim-trailing;
                $prs.body .= new;
                CALLERS::<$*prs> += $prs;
             }
        }
        $prs.body ~= %!templates{ $template }(
            %( :$contents, :$keep-format, :$schema, :$uri-body, :$uri, %config )
        )
    }
    #| The rakudoc block should encompass the output
    #| Config data associated with block is provided to overall process state
    #| If a rakudoc file is embedded via place, then another rakudoc block
    #| will be called. Only allow embedding to three levels to avoid circularity.
    method gen-rakudoc($ast, $parify) {
        my %config = $ast.resolved-config;
        $!current.source-data<rakudoc-config> = %config;
        my $contents = self.contents($ast, $parify);
        # render any tailing lists
        $contents ~= $.complete-item-list ~ $.complete-defn-list
        ~ $.complete-numitem-list ~ $.complete-numdefn-list;
        $*prs.body ~= %!templates<rakudoc>( %( :$contents, %config ) );
    }
    #| A section is invisible to ToC, but is used by scoping
    #| Some output formats may want to handle section, so
    #| embedded RakuDoc are rendered and contents rendered by section template
    method gen-section($ast, $parify) {
        my %config = $.merged-config($ast, 'section');
        my $contents = $.contents($ast, $parify);
        # render any tailing lists
        $contents ~= $.complete-item-list ~ $.complete-defn-list
        ~ $.complete-numitem-list ~ $.complete-numdefn-list;
        my $id = '';
        with %config<id> {
            if self.is-target-unique( $_ ) {
                self.register-target( $_ );
                $id = $_
            }
            else {
                $*prs.warnings.push(
                    "Attempt to register already existing id ｢$_｣ as new target in ｢section｣"
                    ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣.") if %config<error>
            }
        }
        $*prs.body ~= %!templates<section>( %( :$contents, :$id, %config ) )
    }
    #| Table is a captionless paragraph and not added to ToC
    #| unless overriden by toc/headlevel/caption
    #| contents is processed and rendered using table template
    multi method gen-table($ast) {
        my %config = $.merged-config($ast, 'table');
        my $caption = %config<caption>:delete // '';
        my $prs := $*prs;
        my $target = $.name-id($caption) if $caption;
        $!scoped-data.last-title( $target );
        my $id = %config<id>:delete;
        with $id {
            if self.is-target-unique( $_ ) {
                $id = self.register-target( $_ );
            }
            else {
                $prs.warnings.push("Attempt to register already existing id ｢$_｣ as new target in heading ｢$caption｣") if %config<error>
            }
        }
        my $level = %config<headlevel> // 1 ;
        $level = 1 if $level < 1;
        # numeration is attached to contents first
        my $numeration = '';
        if %config<numerate> {
            $numeration ~= PCell.new( :$!register, :id('table_' ~ $target ) );
            $prs.head-numbering.push: ['table_' ~ $target, $level ];
        }
        my $toc = %config<toc>:delete // False;
        # attach numeration to caption and contents separately, allowing template
        # developer to add numeration to caption if wanted by changing the template
        $prs.toc.push(
            { :$caption, :$target, :$level, :$numeration }
        ) if $toc;
        my Bool $procedural = $ast.visual-table.not;
        my @grid;
        my @headers;
        my @rows;
        my $header-rows;
        if $procedural {
            # grid traversing algorithm due to Damian Conway
            # Initially empty grid...
            # How to locate the next empty cell...
            my \find_next_empty = {
                ACROSS => sub (:%at is copy) {
                    # Search leftwards for first empty cell...
                    repeat { %at<col>++ } until !defined @grid[%at<row>][%at<col>];
                    return %at;
                },
                DOWN => sub (:%at is copy) {
                    # Search downwards for first empty cell...
                    repeat { %at<row>++ } until !defined @grid[%at<row>][%at<col>];
                    return %at;
                },
                ROW => sub (:%at is copy) {
                    # Search downwards for first row with an empty cell to the right...
                    # (Note: starts by searching current row before moving down)
                    for %at<row> ..* -> $row {
                        for 0 ..^ %at<col> -> $col {
                            return { :$row, :$col } if !defined @grid[$row][$col];
                        }
                    }
                },
                COLUMN => sub (:%at is copy) {
                    # Search rightwards for first column with an empty cell above...
                    # (Note: starts by searching current column before moving left)
                    for %at<col> ..* -> $col {
                        for 0 ..^ %at<row> -> $row {
                            return { :$row, :$col } if !defined @grid[$row][$col];
                        }
                    }
                },
            }
            # parse row and column directive contents, should be in node.config
            my %POS = :row(0), :col(0);
            my $DIR = 'ACROSS';
            # Track previous action at each step...
            my $prev-was-cell = False; # because we are in grid
            my @cell-context = ( %(), ); # cell context can be set at grid, row, column, or cell level
            # span type only set at cell level
            for <label header align> -> $k {
                @cell-context[*-1]{ $k } = $_ with %config{ $k };
            }
            for $ast.paragraphs -> $grid-instruction {
                unless $grid-instruction.^can('type') {
                    $prs.body ~= $ast.Str;
                    $prs.warnings.push("｢{$grid-instruction.Str}｣ is illegal as an immediate child of a =table")
                    if %config<error>;
                    return
                }
                next if $grid-instruction.type eq 'comment';
                given $grid-instruction.type {
                    when 'cell' {
                        my %cell-config = $grid-instruction.resolved-config;
                        my %payload = %( |@cell-context[*-1], %cell-config );
                        # to be expanded to get-contents
                        %payload<data> = $.contents( $grid-instruction, False );
                        my $span;
                        $span = $_ with %cell-config<span>;
                        with %cell-config<column-span> {
                            $span[0] = $_;
                            $span[1] //= 1
                        }
                        with %cell-config<row-span> {
                            $span[0] //= 1;
                            $span[1] = $_
                        }
                        %payload<span> = $span if $span;
                        # Fill current cell with payload...
                        @grid[%POS<row>][%POS<col>] = %payload;
                        # Reserve the full span of cells specified...
                        if $span {
                            for 0 ..^ $span[0] -> $extra-col {
                                for 0 ..^ $span[1] -> $extra-row {
                                    @grid[%POS<row> + $extra-row][%POS<col> + $extra-col]
                                            //= %( :no-cell, );
                                }
                            }
                        }
                        # Find next empty cell in the fill direction...
                        %POS = find_next_empty{$DIR}(at => %POS);
                    }
                    when 'row' {
                        @cell-context.pop if @cell-context.elems > 1;  # this is only false if the first row/column after =table
                        # Check the contents for metadata
                        @cell-context.push: %( |@cell-context[0], |$grid-instruction.resolved-config );
                        # Start filling across the new row...
                        $DIR = 'ACROSS';
                        # Find the new fill position...
                        if $prev-was-cell {
                            %POS = find_next_empty<ROW>(at => %POS);
                        }
                    }
                    when 'column' {
                        @cell-context.pop if @cell-context.elems > 1;  # this is only false if the first row/column after =table
                        # Check the contents for metadata
                        @cell-context.push: %( |@cell-context[0], |$grid-instruction.resolved-config );

                        # Start filling down the new column...
                        $DIR = 'DOWN';
                        # Find the new fill position...
                        if $prev-was-cell {
                            %POS = find_next_empty<COLUMN>(at => %POS);
                        }
                    }
                    default { # only =cell =row =column allowed after a =grid
                        $prs.body ~= $ast.Str;
                        $prs.warnings.push("｢{$grid-instruction.DEPARSE}｣ is illegal as an immediate child of a =table")
                        if %config<error>;
                        return
                    }
                }
                # Update previous action...
                $prev-was-cell = $grid-instruction.type eq 'cell';
            }
            $header-rows = @grid.grep( *.[0]<header> ).elems;
        }
        else {
            for $ast.paragraphs -> $row {
                next if $row ~~ Str; # rows as strings are row/header separators
                unless $row ~~ RakuAST::Doc::LegacyRow {
                    $prs.body ~= $ast.DEPARSE;
                    $prs.warnings.push("｢{$row.Str}｣ is illegal as an immediate child of a =table")
                    if %config<error>;
                    return
                }
                my @this-row;
                for $row.cells {
                    my ProcessedState $*prs .= new;
                    $.handle( $_ );
                    my $prs := $*prs;
                    @this-row.push: $prs.body;
                    $prs.body .= new;
                    CALLERS::<$*prs> += $prs;
                }
                @rows.push: @this-row
            }
            with %config<header-row> {
                @headers = @rows.shift for ^($_+1);
            }
        }
        $prs.body ~= %!templates<table>.( %(
            :$procedural, :$caption, :$id, :$target, :$level,
            :$header-rows, :@headers, :@rows, :@grid,
            %config ) );
    }
    #| A lower case block generates a warning
    #| DEPARSED Str is rendered with 'unknown' template
    #| Nothing added to ToC
    method gen-unknown-builtin($ast) {
        my %config = $.merged-config( $ast, '*' );
        my $contents = $ast.DEPARSE;
        my $prs := $*prs;
        if $ast.type ~~ any(<
                cell code input output comment head numhead defn item numitem nested para
                rakudoc section pod table formula
            >) { # a known built-in, but to get here the block is unimplemented
            $prs.warnings.push(
                '｢' ~ $ast.type ~ '｣'
                ~ 'is a valid, but unimplemented builtin block'
                ~ " in block