### ----------------------------------------------------
### -- Rakuast::RakuDoc::Render
### -- Licenses: Artistic-2.0
### -- Authors: Richard Hainsworth
### -- File: lib/RakuDoc/Render.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 4
use experimental :rakuast;
use RakuDoc::Processed;
use RakuDoc::Templates;
use RakuDoc::ScopedData;
use RakuDoc::MarkupMeta;
use RakuDoc::PromiseStrings;
use RakuDoc::Numeration;
use LibCurl::Easy;
use Digest::SHA1::Native;
use PrettyDump;
use URI;

enum RDProcDebug <None All AstBlock BlockType Scoping Templates MarkUp>;

class RakuDoc::Processor {
    has %.templates is Template-directory;
    has PCellTracker $.register .= new;
    has RakuDoc::Processed $.current;
    has $.output-format;
    has RakuDoc::ScopedData $!scoped-data .= new;
    #| debug modes that are checked
    has SetHash $!debug-modes .= new;
    #| installed plugins to prevent re-installation
    has SetHash $.installed-plugins .= new;

    multi method debug(RDProcDebug $type --> Nil ) {
        given $type {
            when None {
                $!debug-modes = Nil;
                %!templates.debug = False;
                $!scoped-data.debug = False;
            }
            when All {
                $!debug-modes .= new( RDProcDebug::.values.grep( none( All, None )) );
                %!templates.debug = True;
                $!scoped-data.debug = True;
            }
            when Templates {
                %!templates.debug = True;
                proceed
            }
            when Scoping {
                $!scoped-data.debug = True;
                proceed
            }
            default {
                $!debug-modes{ $type }++
            }
        }
    }
    multi method debug( Str $type --> Nil ) {
        with RDProcDebug::{$type} {
            self.debug( $_ )
        }
        elsif $type ~~ / \s / {
            self.debug( $type.split(/ \s+ /) )
        }
        else { Nil }
    }
    multi method debug( *@types --> Nil ) {
        for @types {
            $.debug( $_ )
        }
    }
    multi method debug {
        $!debug-modes
    }
    method verbose( Str $template ){ # hook onto $!templates
        %!templates.verbose = $template
    }
    method test( $flag ){ # hook onto $!templates
        %!templates.test = $flag
    }
    method pretty( $flag ){ # hook onto $!templates
        %!templates.pretty = $flag
    }
    method add-data( $key, $object ) {
        %!templates.data{ $key } = $object;
    }

    multi submethod TWEAK(:$!output-format = 'txt',
     :$test = False,
     :$pretty = False,
     :$debug = None ) {
        %!templates.source = 'Default text templates' ;
        %!templates = self.default-text-templates;
        %!templates.test = $test;
        %!templates.pretty = $pretty;
        %!templates.helper = self.default-helpers;
        %!templates.escape = -> $s { self.escape( $s ) };
        %!templates.mangle = -> $s { self.mangle( $s ) };
        if $debug ~~ List { self.debug($debug.list) }
        else { self.debug( $debug ) }
    }

    method add-template( Pair $t, :$source ) {
        %!templates.source = $source with $source;
        %!templates{ $t.key } = $t.value
    }
    method add-templates( Hash $tt, :$source! ) {
        %!templates.source = $source with $source;
        for $tt.pairs {
            %!templates{ .key } = .value
        }
    }

    #| renders to a String by default,
    #| but returns ProcessedState object if pre-finalised = True
    multi method render( $ast, :%source-data, :pre-finalized(:$pre-finalised) = False ) {
        $!current .= new(:%source-data, :$!output-format );
        %!templates.data<source-data> := $!current.source-data;
        $!register .= new;
        $!scoped-data .= new;
        $!scoped-data.debug = $!debug-modes{ Scoping }.so;
        my ProcessedState $*prs .= new;
        if $ast ~~ RakuAST::StatementList {
            $ast.rakudoc.map( { $.handle( $_ ) } )
        }
        elsif $ast ~~ List {
            $ast.map( { $.handle( $_ ) } )
        }
        else { return 'Unknown type of AST' }
        $!current += $*prs;
        # neither of the approaches below allow for multi-threading.
        # both fail on xt/055-debug-vals, and -simple-render.
## yields error with Numeric(ProcessedState:D) cant be resolved
#        $ast.rakudoc.map( {
#            $.handle( $_ );
#            $!current += $*prs;
#        }).hyper;
## following yields a different set of errors to .hyper and is MUCH slower!
#        # to get multithreading
#        my @leaves;
#        for $ast.rakudoc {
#            @leaves.push: start { $.handle($_) };
#        }
#        await.allof( @leaves );
#        $!current += $*prs;
        # placed semantic blocks now need triggering
        for $!register.list-unexpanded('semantic').kv -> $id, $spec {
            if $!current.semantics{$spec}:exists {
                $!register.add-payload( :payload( $!current.semantics{$spec}.join ), :id("semantic_$spec") )
            }
            else {
                $!current.warnings.push: "Placement of undefined semantic block $spec"
            }
        }
        # Since the footnote order may only be known at the end
        # footnote numbers are PCells, which need triggering
        self.complete-footnotes;
        # P<toc:>, P<index:> may put PCells into body
        # so ToC and Index need to be rendered and any other PCells triggered
        # toc may contain numbered captions, so the heading-numbers need to be calculated
        self.complete-heading-numerations;
        $!current.rendered-toc = self.complete-toc( :spec( '*' ), :caption( $!current.source-data<toc-caption>));
        my @pcells = $!current.rendered-toc.has-PCells;
        if @pcells.elems {
            $!current.warnings.push( 'In ToC: ' ~ $_ ) for @pcells
        }
        $!current.rendered-toc .= Str;
        #render filtered toc, if any
        for $!register.list-unexpanded('toc').kv -> $id, $spec {
            my $payload = self.complete-toc( :$spec, :caption('')).strip.Str;
            $!register.add-payload(:$payload, :$id)
        }
        # make sure all 'place' elements in the index are strings, not PCells
        stringify-index( $!current.index );
        # render indices
        $!current.rendered-index = self.complete-index( :spec('*'), :caption( $!current.source-data<index-caption> ) );
        @pcells = $!current.rendered-index.has-PCells;
        if @pcells.elems {
            $!current.warnings.push( 'In Index: ' ~ $_ ) for @pcells
        }
        $!current.rendered-index .= Str;
        # place for rendering filtered index
        for $!register.list-unexpanded('index').kv -> $id, $spec {
                $!register.add-payload(:payload( self.complete-index(:$spec, :caption('')).strip.Str ) , :$id);
        }
        $!current.body.strip; # replace expanded PCells with Str
        # all suspended PCells should have been replaced by Str
        # Remaining PCells should trigger warnings
        @pcells = $!current.body.has-PCells;
        if @pcells.elems {
            $!current.warnings.push( $_ ) for @pcells
        }
        $pre-finalised ?? $.current !! $.finalise
    }

    method finalise( --> Str ) {
        $.post-process(
            %!templates<final>( %(
                :body($!current.body.Str),
                :source-data($!current.source-data),
                :front-matter($!current.front-matter),
                :name($!current.name),
                :title($!current.title),
                :title-target($!current.title-target),
                :subtitle($!current.subtitle),
                :modified($!current.modified),
                :rendered-toc($!current.rendered-toc),
                :rendered-index($!current.rendered-index),
                :footnotes(%!templates<footnotes>( %( :footnotes( $!current.footnotes ),) )),
                :warnings(%!templates<warnings>( %(:warnings($!current.warnings),))),
            ) ).Str
        )
    };

    #| This method is used to post process the final rendered output
    #| Use case: change targets to line numbers in a text output
    #| It should be overridden in subclasses for other outputs
    method post-process( Str:D $final --> Str ) {
        return $final unless (%*ENV<POSTPROCESSING>:exists and %*ENV<POSTPROCESSING> == 1);
        my $width = %*ENV<WIDTH> // 80; #$!current.source-data<line-width> // 80;
        my $end-zone = %*ENV<LINE-WIDTH> // 6;#$!current.source-data<line-width> // 6;
        $width = 80 if $width < $end-zone;
        my $remaining = $final;
        my @lines;
        my Bool $hyphen = False;
        my Bool $nl = False;
        while ( $remaining ~~ / ^
                  (\v)
                | ( .**{0..$width} \v )
                | ( .**{ $width - $end-zone } \S+ ) \s{ $nl = True }
                | ( .**{ $width - 1 } ) { $hyphen = $nl = True }
                / ) {
            my $l = ~$/[0];
            $remaining = ~$/.postmatch;
            $l ~= '-' and ( $hyphen = False ) if $hyphen;
            $l ~= "\n" and ( $nl = False ) if $nl;
            @lines.push: $l
        }
        @lines.push( $remaining);
        @lines.join('')
    }

    method compactify( Str:D $s ) {
        self.escape($s.subst(/ \v+ /,' ',:g )
        .subst(/ <?after \S\s> \s+ /, '', :g))
    }

    # Section dealing with plugins
    #| plugins are enabled by calling their .enable method on the processor
    #| typically the enable method will create the plugin's dataspace and add templates
    method add-plugins( @plugin-list ) {
        for @plugin-list -> $plugin {
            next if $plugin.starts-with('#');
            if $plugin (elem) $!installed-plugins {
                note "Attempted to re-install ｢$plugin｣, ignoring duplicate installation";
                next
            }
            else { $!installed-plugins{ $plugin }++ }
            require ::($plugin);
            CATCH {
                note "$plugin is not installed";
                .resume
            }
            try {
                ::($plugin).new.enable( self )
            }
            with $! {
                note "Could not enable «$plugin» in Render. Error: ", .message;
            }
        }
    }

    #| plugins attach information about assets (eg. css/js & cdn for html) as arrays of Str/order tuples
    #| This needs to be gathered from all plugins, and processed for templates
    multi method gather-flatten( @keys, :@reserved ) { $.gather-flatten( $_, :@reserved ) for @keys }
    multi method gather-flatten( $key, :@reserved = () ) {
        my %d := $.templates.data;
        my %valid = %d.pairs
            .grep({ .key ~~ none(@reserved) })
            .grep({ .value.{ $key } ~~ Positional })
            .map( { .key => .value.{ $key } });
        my @p-tuples;
        for %valid.kv -> $plugin, $tuple-list {
            if $tuple-list ~~ Positional {
                for $tuple-list.list {
                    if .[0] ~~ Str && .[1] ~~ Int {
                        @p-tuples.push: $_
                    }
                    else { note "Element ｢$_｣ of config attribute ｢$key｣ for plugin ｢$plugin｣ not a [Str, Int] tuple"}
                }
            }
            else { note "Config attribute ｢$key｣ for plugin ｢$plugin｣ must be a Positional, but got ｢$tuple-list｣"}
        }
        if %d{ $key }:exists { # this is true for css from HTML, add it with zero order.
            @p-tuples.push: [ %d{ $key }, 0]
        }
        %d{ $key } = @p-tuples.sort({ .[1], .[0] }).map( *.[0] ).list;
    }


    #| All handle methods may generate debug reports
    proto method handle( $ast ) {
        say "Handling: { $ast.WHICH.Str.subst(/ \| .+ $/, '') }"
            if $.debug (cont) AstBlock;
        {*}
    }
    multi method handle(Str:D $ast) {
        $*prs.body ~=
            $!scoped-data.verbatim
            ?? ~$ast !! $.compactify( $ast )
    }
    multi method handle(RakuAST::Node:D $ast) {
        $ast.rakudoc.map({ $.handle($_) })
    }
    multi method handle(RakuAST::Doc::Block:D $ast) {
        my $prs := $*prs;
        if $!scoped-data.verbatim {
            $prs.body ~= $ast.set-paragraphs( $ast.paragraphs.map({ $.handle($_) }) ).DEPARSE;
            return
        }
        # When a block is extended, then bare Str should be considered paragraphs
        my Bool $parify = ! ($ast.for or $ast.abbreviated);
        my $type = $ast.type;
        say "Doc::Block type: $type"
            ~ ( ' [for]' if $ast.for )
            ~ ( ' [abbreviated]' if $ast.abbreviated )
            ~ ( ' [extended]' if $parify )
            if $.debug (cont) BlockType;
        # When any block, other than =item or =defn, is started,
        # there may be a list of preceding items or defns, which need to be
        # completed and rendered
        $prs.body ~= $.complete-item-list unless $type eq 'item';
        $prs.body ~= $.complete-defn-list unless $type eq 'defn';
        $prs.body ~= $.complete-numitem-list unless $type eq 'numitem';
        $prs.body ~= $.complete-numdefn-list unless $type eq 'numdefn';

        # Not all Blocks create a new scope. Some change the current scope data
        given $ast.type {
            # =alias
            # Define a RakuDoc macro, scoped to the current block
            when 'alias' { $.manage-alias( $ast ) }
            # =implicit code created by indenting, but by specification is treated as code
            when 'implicit-code' { $.gen-paraish( $ast, 'code', $parify ) }
            # =code
            # =para block, as opposed to unmarked paragraphs in a block
            # not the same as a logical Doc::Paragraph, which has atoms, not paragraphs
            # =nested
            # Nest block contents within the current context
            # unlike =section, does not create block scope
            # =input
            # Pre-formatted sample input
            # =output
            # Pre-formatted sample output
            when any(<code para nested input output>) {
                $.gen-paraish( $ast, $ast.type, $parify )
            }
            # =comment
            # Content to be ignored by all renderers
            when 'comment' { '' }
            # =config
            # Block scope modifications to a block or markup instruction
            when 'config' { $.manage-config($ast); }
            # =formula
            # Render content as LaTex formula
            when 'formula' { $.gen-formula($ast) }
            # =head
            # First-level heading
            # =headN
            # Nth-level heading
            when 'head' { $.gen-headish($ast, $parify) }
            # =numhead
            # First-level numbered heading
            # =numheadN
            # Nth-level numbered heading
            # heading numeration is fixed when TOC is generated and all headers are known
            when 'numhead' { $.gen-headish( $ast, $parify, :template<numhead>, :numerate) }
            # =item
            # First-level list item
            # =itemN
            # Nth-level list item
            when 'item' { $.gen-item($ast, $parify) }
            # =defn
            # Definition of a term
            when 'defn' { $.gen-defn($ast) }
            # =numitem
            # First-level numbered list item
            # =numitemN
            # Nth-level numbered list item
            when 'numitem' { $.gen-numitem($ast, $parify) }
            # =numdefn
            when 'numdefn' { $.gen-defn($ast, :numerate) }
            # =place block, mostly mimics P<>, but allows for TOC and caption
            when 'place' { $.gen-place($ast) }
            # =rakudoc
            # No "ambient" blocks inside
            # could be called if P<> embeds a RakuDoc file
            when 'rakudoc' | 'pod' {
                my $rak-level := $!current.source-data<rakudoc-level>;
                if $!scoped-data.last-starter eq 'original level' {
                    $!scoped-data.start-scope(:starter($_), :title( $!current.source-data<rakudoc-title> ));
                     $rak-level = 1;
                }
                else {
                    $!scoped-data.start-scope(:starter($_), :title( 'embed RakuDoc' ));
                    if ++$rak-level > 5 {
                        $*prs.warnings.push: 'Attempting to embed more than five levels of rakudoc sources. Contents of deeper rakudoc blocks ignored.';
                    }
                }
                $.gen-rakudoc($ast, $parify) unless $rak-level > 5;
                $!scoped-data.end-scope;
                $rak-level -= 1;
            }
            # =pod
            # Legacy version of rakudoc
#           when 'pod' { '' } # when rakudoc differs from pod
            # =section
            # Defines a section
            when 'section' {
                $!scoped-data.start-scope( :starter($_) ); # title will be a Block number
                $.gen-section($ast, $parify);
                $!scoped-data.end-scope;
            }
            # =table
            # Visual or procedural table
            when 'table' { #toc
                $!scoped-data.start-scope( :starter($_) ); # title will be a Block number
                $.gen-table($ast);
                $!scoped-data.end-scope;
            }
            # =cell
            # Contains data in a procedural table
            # =column
            # Start a new column in a procedural table
            # =row
            # Start a new row in a procedural table
            when any(<cell column row>) {
                $prs.warnings.push: qq:to/WARN/;
                The text ｢{ $ast.DEPARSE }｣ may not exist outside a 'table' block.
                WARN
            }
            # RESERVED
            # Semantic blocks (SYNOPSIS, TITLE, etc.)
            when all($_.uniprops) ~~ / Lu / {  #toc
                # in RakuDoc v2 a Semantic block must have all uppercase letters
                $!scoped-data.start-scope( :starter($_) );
                $.gen-semantics($ast, $parify);
                $!scoped-data.end-scope;
            }
            # CustomName
            # User-defined block
            when any($_.uniprops) ~~ / Lu / and any($_.uniprops) ~~ / Ll / { #toc
                # in RakuDoc v2 a Semantic block must have mix of uppercase and lowercase letters
                $!scoped-data.start-scope( :starter($_) );
                $!scoped-data.last-title( $_ );
                $.gen-custom($ast, $parify);
                $!scoped-data.end-scope;
            }
            default { $.gen-unknown-builtin($ast) }
        }
    }
    # RakuDoc declarator block
    multi method handle(RakuAST::Doc::DeclaratorTarget:D $ast) {
        $*prs.warnings.push: qq:to/WARN/;
            ｢{ $ast.DEPARSE }｣ is a declarator block and not rendered for text-based output formats.
            WARN
    }
    my @format-codes = <B H I J K R T U O>;
    multi method handle(RakuAST::Doc::Markup:D $ast) {
        my $letter = $ast.letter;
        my $prs := $*prs;
        say "Doc::Markup letter: $letter" if $.debug (cont) MarkUp;
        my %config = $.merged-config( $, $letter );
        %config<in-code> = $!scoped-data.verbatim;
        if (
            $!scoped-data.verbatim(:called-by) eq <code markup-C markup-V>.any
            )
            && %*ALLOW.defined && !%*ALLOW{ $letter }
            {
            given $letter {
                when 'A' | 'D' | 'F' | 'L' | 'M' | 'P' | 'X' | 'Δ'  {
                    $prs.body ~= $letter ~ self.escape($ast.opener) ~ self.markup-contents($ast);
                    if $ast.meta -> $_ { $prs.body ~= '|' ~ self.markup-contents( $ast, :meta ) }
                    $prs.body ~= self.escape($ast.closer)
                }
                when 'E' {
                    $prs.body ~= $letter ~ self.escape($ast.opener) ~ self.markup-contents($ast);
                    if $ast.meta -> $_ { $prs.body ~= '|' ~ .map(*.key).join('; ') }
                    $prs.body ~= self.escape($ast.closer)
                }
                default { $prs.body ~= self.escape($ast.DEPARSE) }
            }
            return
        }
        # for X<> and to help with warnings
        my $place = $!scoped-data.last-title;
        my $context = $!scoped-data.last-starter;
        given $letter {
            ## Markup codes with only display (format codes), no meta data allowed
            ## meta data via Config is allowed
            when @format-codes {
                my $contents = self.markup-contents($ast);
                $prs.body ~= %!templates{"markup-$letter"}(
                    %( :$contents, %config )
                );
            }
            # Do not eat space inside markup
            when 'S' {
                $prs.body ~= %!templates{"markup-$letter"}(
                    %( :contents( $ast.atoms.join ), %config )
                );
            }
            # Make verbatim, but compactify space
            when any( <C V> ) {
                my %*ALLOW = %( CALLERS::<%*ALLOW> );
                # replace contents of %*ALLOW if so configured
                if %config<allow>:exists {
                    %*ALLOW = Empty;
                    %*ALLOW = %config<allow>
                    .grep({ any(@format-codes) })
                    .map({ $_ => True }).hash;
                }
                $!scoped-data.start-scope(:starter("markup-$letter"), :verbatim);
                my $contents = $.markup-contents($ast);
                $prs.body ~= %!templates{"markup-$letter"}(
                    %( :$contents, %config )
                );
                $!scoped-data.end-scope
            }
            ## Display only, but has side-effects
            #| Footnotes (from N<> markup)
            #| Ordered Array of :$text, :$retTarget, :$fnNumber, :$fnTarget
            #| text is content of footnote, fnNumber is footNote number
            #| fnTarget is link to rendered footnote
            #| retTarget is link to where footnote is defined to link back form footnote
            when 'N' {
                my $id = self.name-id(textify-markup($ast));
                my $contents = self.markup-contents($ast);
                my $retTarget = $id;
                my $fnNumber = PCell.new( :id("fn_num_$id"), :$!register);
                my $fnTarget = "fn_target_$id";
                # fnNumber is changed by complete-footnotes at end of rendering
                $prs.footnotes.push: %( :$contents, :$retTarget, :0fnNumber, :$fnTarget );
                my $rv = %!templates<markup-N>(
                    %( %config, :$retTarget, :$fnNumber, :$fnTarget )
                );
                $prs.body ~= $rv;
            }

            ## Markup codes, optional display and meta data

            # A< DISPLAY-TEXT |  METADATA = ALIAS-NAME >
            # Alias to be replaced by contents of specified V<=alias> directive
            when 'A' {
                my $term = self.markup-contents($ast).Str;
                my $alt = '';
                # check to see if there is a text to over-ride automatic failure message
                if $ast.meta {
                    $alt = $term;
                    $term = $ast.meta.Str
                }
                elsif $term ~~ / ^ (<-[ | ]>+) \| (.+) $ / {
                    $alt = ~$0.trim;
                    $term = ~$1.trim
                }
                my $contents;
                if $!scoped-data.aliases{ $term }:exists {
                    $contents = $!scoped-data.aliases{ $term }
                }
                else {
                    $contents = $term;
                    $contents = $alt if $alt;
                    $prs.warnings.push(
                        "Unknown or as yet undeclared alias ｢$term｣"
                        ~ " in block ｢$context｣ with heading ｢$place｣"
                        ~ ( $alt ?? " over-riden by ｢$alt｣" !! ''  ))
                        if %config<error>
                }
                $prs.body ~ %!templates<markup-A>( %( :$contents, %config ) )
            }
            # E< DISPLAY-TEXT |  METADATA = HTML/UNICODE-ENTITIES >
            # Entity (HTML or Unicode) description ( E<entity1;entity2; multi,glyph;...> )
            # included with format codes
            when 'E' {
                my $contents = $ast.meta.map(*.values).join;
                $prs.body ~= %!templates<markup-E>(
                    %( :$contents, %config )
                );
            }
            # F< DISPLAY-TEXT |  METADATA = LATEX-FORM >
            # Formula inline content ( F<ALT|LaTex notation> )
            # At a minimum, only the ALT text should be rendered. But the metadata is passed to the
            # template, so the template can be adapted to render the LATEX formula.
            when 'F' {
                my $formula = $.markup-contents($ast).Str;
                my $contents = $formula;
                $formula = $ast.meta if $ast.meta;
                $prs.body ~= %!templates<markup-F>( %( :$contents, :$formula, %config ) )
            }
            # L< DISPLAY-TEXT |  METADATA = TARGET-URI >
            # Link ( L<display text|destination URI> )
            when 'L' {
                my $link-label = self.markup-contents($ast).Str;
                my $entry = $ast.meta;
                with $entry[0] {
                    $entry = $entry[0].Str
                }
                else {
                    $entry = $link-label
                }
                my $target;
                my $extra = '';
                my $type;
                if $!current.links{$entry}:exists {
                    ($target, $type, $extra) = $!current.links{$entry}<target type extra>
                }
                else {
                    given $entry {
                        # remote links first, if # in link, that will be handled by destination
                        when / ^ 'http://' | ^ 'https://' / {
                            $target = $_;
                            $type = 'external';
                        }
                        # next deal with internal links
                        when / ^ '#' $<tgt> = (.+) $ / {
                            $target = $.local-heading( ~$<tgt>);
                            $type = 'internal';
                        }
                        when / ^ (.+?) '#' (.+) $ / {
                            $extra = ~$1;
                            $target = ~$0.subst(/'::'/, '/', :g); # only subst :: in file part
                            $type = 'local';
                        }
                        when / ^ 'defn:' $<term> = (.+) $ / {
                            $type = 'defn';
                            $link-label = ~$<term>;
                            # get definition from Processed state, or make a PCell
                            my %definitions = $prs.definitions;
                            if %definitions{ $link-label }:exists {
                                $extra = %definitions{ $link-label }[0];
                                $target = %definitions{ $link-label }[1]
                            }
                            else {
                                $extra = PCell.new( :$!register, :id($link-label));
                                $target = PCell.new( :$!register, :id($link-label ~ '_target' ))
                            }
                        }
                        when '' { # this is an error condition
                            $target = '';
                            $type = 'internal';
                            $extra = '';
                        }
                        default {
                            $target = $entry;
                            $type = 'local';
                            $extra = '';
                        }
                    }
                }
                $!current.links{$_} = %(:$target, :$link-label, :$type, :$extra);
                my $rv = %!templates{"markup-$letter"}(
                    %( :$target, :$link-label, :$type, :$extra, :$!output-format, %config)
                );
                $prs.body ~= $rv;
            }
            # P< DISPLAY-TEXT |  METADATA = REPLACEMENT-URI >
            # Placement link
            when 'P' {
                # The contents of P<> markup must be a Str.
                my $uri = self.markup-contents($ast).Str;
                # check to see if there is a text to over-ride automatic failure message
                if $ast.meta {
                    %config<fallback> = $uri;
                    $uri = $ast.meta.Str
                }
                elsif $uri ~~ / ^ (<-[ | ]>+) \| (.+) $ / {
                    %config<fallback> = ~$0;
                    $uri = ~$1
                }
                $.make-placement(:$uri, :%config, :template<markup-P>);
            }

            ## Markup codes, mandatory display and meta data
            # D< DISPLAY-TEXT |  METADATA = SYNONYMS >
            # Definition inline ( D<term being defined|synonym1; synonym2> )
            when 'D' {
                my $contents = self.markup-contents($ast).Str; # is the term, may not have embedded markup
                my $meta = RakuDoc::MarkupMeta.parse( ~$ast.meta.trim, actions => RMActions.new );
                my @terms = $contents, ;
                if $meta.so {
                    given $meta.made<type> {
                        when 'plain-string' or 'plain-string-array' { # treat as verbatim
                            @terms.push: ~$/
                        }
                        when 'array-of-ps-arrays' { # add back ,
                            @terms.append: $meta.made<value>>>.join(', ')
                        }
                   }
                }
                else {
                    $prs.warnings.push("Ignored unparsable definition synonyms ｢{ ~$ast.meta }｣"
                        ~ " in block ｢$context｣ with heading ｢$place｣.")
                        if %config<error>
                }
                my $rv = %!templates{"markup-$letter"}(
                    %( :$contents, %config )
                );
                $prs.inline-defns.append: @terms;
                $prs.body ~= $rv
            }
            # Δ< DISPLAY-TEXT |  METADATA = VERSION-ETC >
            # Delta note ( Δ<visible text|version; Notification text> )
            when 'Δ' {
                my $contents = self.markup-contents($ast);
                my $meta = '';
                my $note = '';
                my $versions = '';
                if $ast.meta {
                    $meta = $ast.meta.Str
                }
                elsif $contents ~~ / ^ (<-[|]>+) \| (.+) $ / {
                    $contents = ~$0;
                    $meta = ~$1
                }
                if $meta {
                    if $meta ~~ / ^ (<-[;]>+) ';' (.+) $ / {
                        $versions = ~$0;
                        $note = ~$1
                    }
                    else { $versions = $meta }
                    $prs.body ~=  %!templates{"markup-Δ"}(
                        %( :$contents, :$versions, :$note, %config)
                    )
                }
                else {
                    $prs.warnings.push("Δ<> markup ignored because it has no version/note content ｢{ ~$ast.DEPARSE }｣"
                        ~ " in block ｢$context｣ with heading ｢$place｣.")
                        if %config<error>;
                    # treat as verbatim text
                    $prs.body ~= %!templates<markup-V>( %( :$contents, %config ))
                }
            }
            # M< DISPLAY-TEXT |  METADATA = WHATEVER >
            # Markup extra ( M<display text|functionality;param,sub-type;...>)
            when 'M' {
                my $contents = self.markup-contents($ast);
                unless $ast.meta {
                    $prs.body ~= %!templates<markup-M>( %(:$contents ) );
                    $prs.warnings.push("Markup-M failed: no meta information. Got ｢{ $ast.DEPARSE }｣")
                        if %config<error>;
                    return
                }
                my $meta = RakuDoc::MarkupMeta.parse( $ast.meta, actions => RMActions.new ).made<value>;
                my $target = self.index-id(:$contents);
                my $template = $meta[0].Str;
                if any($template.uniprops) ~~ / Lu / and any($template.uniprops) ~~ / Ll / {
                    # template has an acceptable custom template spelling
                    if %!templates{ $template }:exists {
                        $prs.body ~= %!templates{ $template }( %(:$contents, :$meta ) );
                    }
                    else {
                        $prs.body ~= %!templates<markup-M>( %(:$contents, :$target ) );
                        $prs.warnings.push("Markup-M failed: template ｢$template｣ does not exist. Got ｢{ $ast.DEPARSE }｣")
                        if %config<error>
                    }
                }
                else {
                    # template is spelt like a SEMANTIC or builtin
                    $prs.body ~= %!templates<markup-M>( %(:$contents ) );
                    $prs.warnings.push("Markup-M failed: first meta string must conform to Custom template spelling. Got ｢{ $ast.DEPARSE }｣")
                        if %config<error>
                }
            }
            # X< DISPLAY-TEXT |  METADATA = INDEX-ENTRY >
            # Index entry ( X<display text|entry,subentry;...>)
            #| Index (from X<> markup)
            #| Hash entry => Hash of :refs, :sub-index
            #| :sub-index (maybe empty) is Hash of sub-entry => :refs, :sub-index
            #| :refs is Array of (Hash :target, :place, :is-header)
            #| :target is for link, :place is section name
            #| :is-header because X<> in headings treated differently to ordinary text
            when 'X' {
                my $contents = self.markup-contents($ast);
                #| if not in a head, then in-head is '', which has a False truth value
                my Bool $is-in-heading = $!scoped-data.in-head.so;
                # stringify contents without processing for targets
                my $meta = RakuDoc::MarkupMeta.parse( $ast.meta.trim, actions => RMActions.new );
                # if in a heading, then the scoped is-head attribute is a target for that title
                my $target = $is-in-heading ?? $!scoped-data.in