+++ /dev/null
-#!/usr/bin/perl
-
-#
-# Markdown -- A text-to-HTML conversion tool for web writers
-#
-# Copyright (c) 2004 John Gruber
-# <http://daringfireball.net/projects/markdown/>
-#
-
-
-package Markdown;
-require 5.006_000;
-use strict;
-use warnings;
-
-use Digest::MD5 qw(md5_hex);
-use vars qw($VERSION);
-$VERSION = '1.0.1';
-# Tue 14 Dec 2004
-
-## Disabled; causes problems under Perl 5.6.1:
-# use utf8;
-# binmode( STDOUT, ":utf8" ); # c.f.: http://acis.openlib.org/dev/perl-unicode-struggle.html
-
-
-#
-# Global default settings:
-#
-my $g_empty_element_suffix = " />"; # Change to ">" for HTML output
-my $g_tab_width = 4;
-
-
-#
-# Globals:
-#
-
-# Regex to match balanced [brackets]. See Friedl's
-# "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
-my $g_nested_brackets;
-$g_nested_brackets = qr{
- (?> # Atomic matching
- [^\[\]]+ # Anything other than brackets
- |
- \[
- (??{ $g_nested_brackets }) # Recursive set of nested brackets
- \]
- )*
-}x;
-
-
-# Table of hash values for escaped characters:
-my %g_escape_table;
-foreach my $char (split //, '\\`*_{}[]()>#+-.!') {
- $g_escape_table{$char} = md5_hex($char);
-}
-
-
-# Global hashes, used by various utility routines
-my %g_urls;
-my %g_titles;
-my %g_html_blocks;
-
-# Used to track when we're inside an ordered or unordered list
-# (see _ProcessListItems() for details):
-my $g_list_level = 0;
-
-
-#### Blosxom plug-in interface ##########################################
-
-# Set $g_blosxom_use_meta to 1 to use Blosxom's meta plug-in to determine
-# which posts Markdown should process, using a "meta-markup: markdown"
-# header. If it's set to 0 (the default), Markdown will process all
-# entries.
-my $g_blosxom_use_meta = 0;
-
-sub start { 1; }
-sub story {
- my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
-
- if ( (! $g_blosxom_use_meta) or
- (defined($meta::markup) and ($meta::markup =~ /^\s*markdown\s*$/i))
- ){
- $$body_ref = Markdown($$body_ref);
- }
- 1;
-}
-
-
-#### Movable Type plug-in interface #####################################
-eval {require MT}; # Test to see if we're running in MT.
-unless ($@) {
- require MT;
- import MT;
- require MT::Template::Context;
- import MT::Template::Context;
-
- eval {require MT::Plugin}; # Test to see if we're running >= MT 3.0.
- unless ($@) {
- require MT::Plugin;
- import MT::Plugin;
- my $plugin = new MT::Plugin({
- name => "Markdown",
- description => "A plain-text-to-HTML formatting plugin. (Version: $VERSION)",
- doc_link => 'http://daringfireball.net/projects/markdown/'
- });
- MT->add_plugin( $plugin );
- }
-
- MT::Template::Context->add_container_tag(MarkdownOptions => sub {
- my $ctx = shift;
- my $args = shift;
- my $builder = $ctx->stash('builder');
- my $tokens = $ctx->stash('tokens');
-
- if (defined ($args->{'output'}) ) {
- $ctx->stash('markdown_output', lc $args->{'output'});
- }
-
- defined (my $str = $builder->build($ctx, $tokens) )
- or return $ctx->error($builder->errstr);
- $str; # return value
- });
-
- MT->add_text_filter('markdown' => {
- label => 'Markdown',
- docs => 'http://daringfireball.net/projects/markdown/',
- on_format => sub {
- my $text = shift;
- my $ctx = shift;
- my $raw = 0;
- if (defined $ctx) {
- my $output = $ctx->stash('markdown_output');
- if (defined $output && $output =~ m/^html/i) {
- $g_empty_element_suffix = ">";
- $ctx->stash('markdown_output', '');
- }
- elsif (defined $output && $output eq 'raw') {
- $raw = 1;
- $ctx->stash('markdown_output', '');
- }
- else {
- $raw = 0;
- $g_empty_element_suffix = " />";
- }
- }
- $text = $raw ? $text : Markdown($text);
- $text;
- },
- });
-
- # If SmartyPants is loaded, add a combo Markdown/SmartyPants text filter:
- my $smartypants;
-
- {
- no warnings "once";
- $smartypants = $MT::Template::Context::Global_filters{'smarty_pants'};
- }
-
- if ($smartypants) {
- MT->add_text_filter('markdown_with_smartypants' => {
- label => 'Markdown With SmartyPants',
- docs => 'http://daringfireball.net/projects/markdown/',
- on_format => sub {
- my $text = shift;
- my $ctx = shift;
- if (defined $ctx) {
- my $output = $ctx->stash('markdown_output');
- if (defined $output && $output eq 'html') {
- $g_empty_element_suffix = ">";
- }
- else {
- $g_empty_element_suffix = " />";
- }
- }
- $text = Markdown($text);
- $text = $smartypants->($text, '1');
- },
- });
- }
-}
-else {
-#### BBEdit/command-line text filter interface ##########################
-# Needs to be hidden from MT (and Blosxom when running in static mode).
-
- # We're only using $blosxom::version once; tell Perl not to warn us:
- no warnings 'once';
- unless ( defined($blosxom::version) ) {
- use warnings;
-
- #### Check for command-line switches: #################
- my %cli_opts;
- use Getopt::Long;
- Getopt::Long::Configure('pass_through');
- GetOptions(\%cli_opts,
- 'version',
- 'shortversion',
- 'html4tags',
- );
- if ($cli_opts{'version'}) { # Version info
- print "\nThis is Markdown, version $VERSION.\n";
- print "Copyright 2004 John Gruber\n";
- print "http://daringfireball.net/projects/markdown/\n\n";
- exit 0;
- }
- if ($cli_opts{'shortversion'}) { # Just the version number string.
- print $VERSION;
- exit 0;
- }
- if ($cli_opts{'html4tags'}) { # Use HTML tag style instead of XHTML
- $g_empty_element_suffix = ">";
- }
-
-
- #### Process incoming text: ###########################
- my $text;
- {
- local $/; # Slurp the whole file
- $text = <>;
- }
- print Markdown($text);
- }
-}
-
-
-
-sub Markdown {
-#
-# Main function. The order in which other subs are called here is
-# essential. Link and image substitutions need to happen before
-# _EscapeSpecialChars(), so that any *'s or _'s in the <a>
-# and <img> tags get encoded.
-#
- my $text = shift;
-
- # Clear the global hashes. If we don't clear these, you get conflicts
- # from other articles when generating a page which contains more than
- # one article (e.g. an index page that shows the N most recent
- # articles):
- %g_urls = ();
- %g_titles = ();
- %g_html_blocks = ();
-
-
- # Standardize line endings:
- $text =~ s{\r\n}{\n}g; # DOS to Unix
- $text =~ s{\r}{\n}g; # Mac to Unix
-
- # Make sure $text ends with a couple of newlines:
- $text .= "\n\n";
-
- # Convert all tabs to spaces.
- $text = _Detab($text);
-
- # Strip any lines consisting only of spaces and tabs.
- # This makes subsequent regexen easier to write, because we can
- # match consecutive blank lines with /\n+/ instead of something
- # contorted like /[ \t]*\n+/ .
- $text =~ s/^[ \t]+$//mg;
-
- # Turn block-level HTML blocks into hash entries
- $text = _HashHTMLBlocks($text);
-
- # Strip link definitions, store in hashes.
- $text = _StripLinkDefinitions($text);
-
- $text = _RunBlockGamut($text);
-
- $text = _UnescapeSpecialChars($text);
-
- return $text . "\n";
-}
-
-
-sub _StripLinkDefinitions {
-#
-# Strips link definitions from text, stores the URLs and titles in
-# hash references.
-#
- my $text = shift;
- my $less_than_tab = $g_tab_width - 1;
-
- # Link defs are in the form: ^[id]: url "optional title"
- while ($text =~ s{
- ^[ ]{0,$less_than_tab}\[(.+)\]: # id = $1
- [ \t]*
- \n? # maybe *one* newline
- [ \t]*
- <?(\S+?)>? # url = $2
- [ \t]*
- \n? # maybe one newline
- [ \t]*
- (?:
- (?<=\s) # lookbehind for whitespace
- ["(]
- (.+?) # title = $3
- [")]
- [ \t]*
- )? # title is optional
- (?:\n+|\Z)
- }
- {}mx) {
- $g_urls{lc $1} = _EncodeAmpsAndAngles( $2 ); # Link IDs are case-insensitive
- if ($3) {
- $g_titles{lc $1} = $3;
- $g_titles{lc $1} =~ s/"/"/g;
- }
- }
-
- return $text;
-}
-
-
-sub _HashHTMLBlocks {
- my $text = shift;
- my $less_than_tab = $g_tab_width - 1;
-
- # Hashify HTML blocks:
- # We only want to do this for block-level HTML tags, such as headers,
- # lists, and tables. That's because we still want to wrap <p>s around
- # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
- # phrase emphasis, and spans. The list of tags we're looking for is
- # hard-coded:
- my $block_tags_a = qr/p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del/;
- my $block_tags_b = qr/p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math/;
-
- # First, look for nested blocks, e.g.:
- # <div>
- # <div>
- # tags for inner block must be indented.
- # </div>
- # </div>
- #
- # The outermost tags must start at the left margin for this to match, and
- # the inner nested divs must be indented.
- # We need to do this before the next, more liberal match, because the next
- # match will start at the first `<div>` and stop at the first `</div>`.
- $text =~ s{
- ( # save in $1
- ^ # start of line (with /m)
- <($block_tags_a) # start tag = $2
- \b # word break
- (.*\n)*? # any number of lines, minimally matching
- </\2> # the matching end tag
- [ \t]* # trailing spaces/tabs
- (?=\n+|\Z) # followed by a newline or end of document
- )
- }{
- my $key = md5_hex($1);
- $g_html_blocks{$key} = $1;
- "\n\n" . $key . "\n\n";
- }egmx;
-
-
- #
- # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
- #
- $text =~ s{
- ( # save in $1
- ^ # start of line (with /m)
- <($block_tags_b) # start tag = $2
- \b # word break
- (.*\n)*? # any number of lines, minimally matching
- .*</\2> # the matching end tag
- [ \t]* # trailing spaces/tabs
- (?=\n+|\Z) # followed by a newline or end of document
- )
- }{
- my $key = md5_hex($1);
- $g_html_blocks{$key} = $1;
- "\n\n" . $key . "\n\n";
- }egmx;
- # Special case just for <hr />. It was easier to make a special case than
- # to make the other regex more complicated.
- $text =~ s{
- (?:
- (?<=\n\n) # Starting after a blank line
- | # or
- \A\n? # the beginning of the doc
- )
- ( # save in $1
- [ ]{0,$less_than_tab}
- <(hr) # start tag = $2
- \b # word break
- ([^<>])*? #
- /?> # the matching end tag
- [ \t]*
- (?=\n{2,}|\Z) # followed by a blank line or end of document
- )
- }{
- my $key = md5_hex($1);
- $g_html_blocks{$key} = $1;
- "\n\n" . $key . "\n\n";
- }egx;
-
- # Special case for standalone HTML comments:
- $text =~ s{
- (?:
- (?<=\n\n) # Starting after a blank line
- | # or
- \A\n? # the beginning of the doc
- )
- ( # save in $1
- [ ]{0,$less_than_tab}
- (?s:
- <!
- (--.*?--\s*)+
- >
- )
- [ \t]*
- (?=\n{2,}|\Z) # followed by a blank line or end of document
- )
- }{
- my $key = md5_hex($1);
- $g_html_blocks{$key} = $1;
- "\n\n" . $key . "\n\n";
- }egx;
-
-
- return $text;
-}
-
-
-sub _RunBlockGamut {
-#
-# These are all the transformations that form block-level
-# tags like paragraphs, headers, and list items.
-#
- my $text = shift;
-
- $text = _DoHeaders($text);
-
- # Do Horizontal Rules:
- $text =~ s{^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$}{\n<hr$g_empty_element_suffix\n}gmx;
- $text =~ s{^[ ]{0,2}([ ]? -[ ]?){3,}[ \t]*$}{\n<hr$g_empty_element_suffix\n}gmx;
- $text =~ s{^[ ]{0,2}([ ]? _[ ]?){3,}[ \t]*$}{\n<hr$g_empty_element_suffix\n}gmx;
-
- $text = _DoLists($text);
-
- $text = _DoCodeBlocks($text);
-
- $text = _DoBlockQuotes($text);
-
- # We already ran _HashHTMLBlocks() before, in Markdown(), but that
- # was to escape raw HTML in the original Markdown source. This time,
- # we're escaping the markup we've just created, so that we don't wrap
- # <p> tags around block-level tags.
- $text = _HashHTMLBlocks($text);
-
- $text = _FormParagraphs($text);
-
- return $text;
-}
-
-
-sub _RunSpanGamut {
-#
-# These are all the transformations that occur *within* block-level
-# tags like paragraphs, headers, and list items.
-#
- my $text = shift;
-
- $text = _DoCodeSpans($text);
-
- $text = _EscapeSpecialChars($text);
-
- # Process anchor and image tags. Images must come first,
- # because ![foo][f] looks like an anchor.
- $text = _DoImages($text);
- $text = _DoAnchors($text);
-
- # Make links out of things like `<http://example.com/>`
- # Must come after _DoAnchors(), because you can use < and >
- # delimiters in inline links like [this](<url>).
- $text = _DoAutoLinks($text);
-
- $text = _EncodeAmpsAndAngles($text);
-
- $text = _DoItalicsAndBold($text);
-
- # Do hard breaks:
- $text =~ s/ {2,}\n/ <br$g_empty_element_suffix\n/g;
-
- return $text;
-}
-
-
-sub _EscapeSpecialChars {
- my $text = shift;
- my $tokens ||= _TokenizeHTML($text);
-
- $text = ''; # rebuild $text from the tokens
-# my $in_pre = 0; # Keep track of when we're inside <pre> or <code> tags.
-# my $tags_to_skip = qr!<(/?)(?:pre|code|kbd|script|math)[\s>]!;
-
- foreach my $cur_token (@$tokens) {
- if ($cur_token->[0] eq "tag") {
- # Within tags, encode * and _ so they don't conflict
- # with their use in Markdown for italics and strong.
- # We're replacing each such character with its
- # corresponding MD5 checksum value; this is likely
- # overkill, but it should prevent us from colliding
- # with the escape values by accident.
- $cur_token->[1] =~ s! \* !$g_escape_table{'*'}!gx;
- $cur_token->[1] =~ s! _ !$g_escape_table{'_'}!gx;
- $text .= $cur_token->[1];
- } else {
- my $t = $cur_token->[1];
- $t = _EncodeBackslashEscapes($t);
- $text .= $t;
- }
- }
- return $text;
-}
-
-
-sub _DoAnchors {
-#
-# Turn Markdown link shortcuts into XHTML <a> tags.
-#
- my $text = shift;
-
- #
- # First, handle reference-style links: [link text] [id]
- #
- $text =~ s{
- ( # wrap whole match in $1
- \[
- ($g_nested_brackets) # link text = $2
- \]
-
- [ ]? # one optional space
- (?:\n[ ]*)? # one optional newline followed by spaces
-
- \[
- (.*?) # id = $3
- \]
- )
- }{
- my $result;
- my $whole_match = $1;
- my $link_text = $2;
- my $link_id = lc $3;
-
- if ($link_id eq "") {
- $link_id = lc $link_text; # for shortcut links like [this][].
- }
-
- if (defined $g_urls{$link_id}) {
- my $url = $g_urls{$link_id};
- $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
- $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
- $result = "<a href=\"$url\"";
- if ( defined $g_titles{$link_id} ) {
- my $title = $g_titles{$link_id};
- $title =~ s! \* !$g_escape_table{'*'}!gx;
- $title =~ s! _ !$g_escape_table{'_'}!gx;
- $result .= " title=\"$title\"";
- }
- $result .= ">$link_text</a>";
- }
- else {
- $result = $whole_match;
- }
- $result;
- }xsge;
-
- #
- # Next, inline-style links: [link text](url "optional title")
- #
- $text =~ s{
- ( # wrap whole match in $1
- \[
- ($g_nested_brackets) # link text = $2
- \]
- \( # literal paren
- [ \t]*
- <?(.*?)>? # href = $3
- [ \t]*
- ( # $4
- (['"]) # quote char = $5
- (.*?) # Title = $6
- \5 # matching quote
- )? # title is optional
- \)
- )
- }{
- my $result;
- my $whole_match = $1;
- my $link_text = $2;
- my $url = $3;
- my $title = $6;
-
- $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
- $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
- $result = "<a href=\"$url\"";
-
- if (defined $title) {
- $title =~ s/"/"/g;
- $title =~ s! \* !$g_escape_table{'*'}!gx;
- $title =~ s! _ !$g_escape_table{'_'}!gx;
- $result .= " title=\"$title\"";
- }
-
- $result .= ">$link_text</a>";
-
- $result;
- }xsge;
-
- return $text;
-}
-
-
-sub _DoImages {
-#
-# Turn Markdown image shortcuts into <img> tags.
-#
- my $text = shift;
-
- #
- # First, handle reference-style labeled images: ![alt text][id]
- #
- $text =~ s{
- ( # wrap whole match in $1
- !\[
- (.*?) # alt text = $2
- \]
-
- [ ]? # one optional space
- (?:\n[ ]*)? # one optional newline followed by spaces
-
- \[
- (.*?) # id = $3
- \]
-
- )
- }{
- my $result;
- my $whole_match = $1;
- my $alt_text = $2;
- my $link_id = lc $3;
-
- if ($link_id eq "") {
- $link_id = lc $alt_text; # for shortcut links like ![this][].
- }
-
- $alt_text =~ s/"/"/g;
- if (defined $g_urls{$link_id}) {
- my $url = $g_urls{$link_id};
- $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
- $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
- $result = "<img src=\"$url\" alt=\"$alt_text\"";
- if (defined $g_titles{$link_id}) {
- my $title = $g_titles{$link_id};
- $title =~ s! \* !$g_escape_table{'*'}!gx;
- $title =~ s! _ !$g_escape_table{'_'}!gx;
- $result .= " title=\"$title\"";
- }
- $result .= $g_empty_element_suffix;
- }
- else {
- # If there's no such link ID, leave intact:
- $result = $whole_match;
- }
-
- $result;
- }xsge;
-
- #
- # Next, handle inline images: 
- # Don't forget: encode * and _
-
- $text =~ s{
- ( # wrap whole match in $1
- !\[
- (.*?) # alt text = $2
- \]
- \( # literal paren
- [ \t]*
- <?(\S+?)>? # src url = $3
- [ \t]*
- ( # $4
- (['"]) # quote char = $5
- (.*?) # title = $6
- \5 # matching quote
- [ \t]*
- )? # title is optional
- \)
- )
- }{
- my $result;
- my $whole_match = $1;
- my $alt_text = $2;
- my $url = $3;
- my $title = '';
- if (defined($6)) {
- $title = $6;
- }
-
- $alt_text =~ s/"/"/g;
- $title =~ s/"/"/g;
- $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
- $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
- $result = "<img src=\"$url\" alt=\"$alt_text\"";
- if (defined $title) {
- $title =~ s! \* !$g_escape_table{'*'}!gx;
- $title =~ s! _ !$g_escape_table{'_'}!gx;
- $result .= " title=\"$title\"";
- }
- $result .= $g_empty_element_suffix;
-
- $result;
- }xsge;
-
- return $text;
-}
-
-
-sub _DoHeaders {
- my $text = shift;
-
- # Setext-style headers:
- # Header 1
- # ========
- #
- # Header 2
- # --------
- #
- $text =~ s{ ^(.+)[ \t]*\n=+[ \t]*\n+ }{
- "<h1>" . _RunSpanGamut($1) . "</h1>\n\n";
- }egmx;
-
- $text =~ s{ ^(.+)[ \t]*\n-+[ \t]*\n+ }{
- "<h2>" . _RunSpanGamut($1) . "</h2>\n\n";
- }egmx;
-
-
- # atx-style headers:
- # # Header 1
- # ## Header 2
- # ## Header 2 with closing hashes ##
- # ...
- # ###### Header 6
- #
- $text =~ s{
- ^(\#{1,6}) # $1 = string of #'s
- [ \t]*
- (.+?) # $2 = Header text
- [ \t]*
- \#* # optional closing #'s (not counted)
- \n+
- }{
- my $h_level = length($1);
- "<h$h_level>" . _RunSpanGamut($2) . "</h$h_level>\n\n";
- }egmx;
-
- return $text;
-}
-
-
-sub _DoLists {
-#
-# Form HTML ordered (numbered) and unordered (bulleted) lists.
-#
- my $text = shift;
- my $less_than_tab = $g_tab_width - 1;
-
- # Re-usable patterns to match list item bullets and number markers:
- my $marker_ul = qr/[*+-]/;
- my $marker_ol = qr/\d+[.]/;
- my $marker_any = qr/(?:$marker_ul|$marker_ol)/;
-
- # Re-usable pattern to match any entirel ul or ol list:
- my $whole_list = qr{
- ( # $1 = whole list
- ( # $2
- [ ]{0,$less_than_tab}
- (${marker_any}) # $3 = first list item marker
- [ \t]+
- )
- (?s:.+?)
- ( # $4
- \z
- |
- \n{2,}
- (?=\S)
- (?! # Negative lookahead for another list item marker
- [ \t]*
- ${marker_any}[ \t]+
- )
- )
- )
- }mx;
-
- # We use a different prefix before nested lists than top-level lists.
- # See extended comment in _ProcessListItems().
- #
- # Note: There's a bit of duplication here. My original implementation
- # created a scalar regex pattern as the conditional result of the test on
- # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
- # substitution once, using the scalar as the pattern. This worked,
- # everywhere except when running under MT on my hosting account at Pair
- # Networks. There, this caused all rebuilds to be killed by the reaper (or
- # perhaps they crashed, but that seems incredibly unlikely given that the
- # same script on the same server ran fine *except* under MT. I've spent
- # more time trying to figure out why this is happening than I'd like to
- # admit. My only guess, backed up by the fact that this workaround works,
- # is that Perl optimizes the substition when it can figure out that the
- # pattern will never change, and when this optimization isn't on, we run
- # afoul of the reaper. Thus, the slightly redundant code to that uses two
- # static s/// patterns rather than one conditional pattern.
-
- if ($g_list_level) {
- $text =~ s{
- ^
- $whole_list
- }{
- my $list = $1;
- my $list_type = ($3 =~ m/$marker_ul/) ? "ul" : "ol";
- # Turn double returns into triple returns, so that we can make a
- # paragraph for the last item in a list, if necessary:
- $list =~ s/\n{2,}/\n\n\n/g;
- my $result = _ProcessListItems($list, $marker_any);
- $result = "<$list_type>\n" . $result . "</$list_type>\n";
- $result;
- }egmx;
- }
- else {
- $text =~ s{
- (?:(?<=\n\n)|\A\n?)
- $whole_list
- }{
- my $list = $1;
- my $list_type = ($3 =~ m/$marker_ul/) ? "ul" : "ol";
- # Turn double returns into triple returns, so that we can make a
- # paragraph for the last item in a list, if necessary:
- $list =~ s/\n{2,}/\n\n\n/g;
- my $result = _ProcessListItems($list, $marker_any);
- $result = "<$list_type>\n" . $result . "</$list_type>\n";
- $result;
- }egmx;
- }
-
-
- return $text;
-}
-
-
-sub _ProcessListItems {
-#
-# Process the contents of a single ordered or unordered list, splitting it
-# into individual list items.
-#
-
- my $list_str = shift;
- my $marker_any = shift;
-
-
- # The $g_list_level global keeps track of when we're inside a list.
- # Each time we enter a list, we increment it; when we leave a list,
- # we decrement. If it's zero, we're not in a list anymore.
- #
- # We do this because when we're not inside a list, we want to treat
- # something like this:
- #
- # I recommend upgrading to version
- # 8. Oops, now this line is treated
- # as a sub-list.
- #
- # As a single paragraph, despite the fact that the second line starts
- # with a digit-period-space sequence.
- #
- # Whereas when we're inside a list (or sub-list), that line will be
- # treated as the start of a sub-list. What a kludge, huh? This is
- # an aspect of Markdown's syntax that's hard to parse perfectly
- # without resorting to mind-reading. Perhaps the solution is to
- # change the syntax rules such that sub-lists must start with a
- # starting cardinal number; e.g. "1." or "a.".
-
- $g_list_level++;
-
- # trim trailing blank lines:
- $list_str =~ s/\n{2,}\z/\n/;
-
-
- $list_str =~ s{
- (\n)? # leading line = $1
- (^[ \t]*) # leading whitespace = $2
- ($marker_any) [ \t]+ # list marker = $3
- ((?s:.+?) # list item text = $4
- (\n{1,2}))
- (?= \n* (\z | \2 ($marker_any) [ \t]+))
- }{
- my $item = $4;
- my $leading_line = $1;
- my $leading_space = $2;
-
- if ($leading_line or ($item =~ m/\n{2,}/)) {
- $item = _RunBlockGamut(_Outdent($item));
- }
- else {
- # Recursion for sub-lists:
- $item = _DoLists(_Outdent($item));
- chomp $item;
- $item = _RunSpanGamut($item);
- }
-
- "<li>" . $item . "</li>\n";
- }egmx;
-
- $g_list_level--;
- return $list_str;
-}
-
-
-
-sub _DoCodeBlocks {
-#
-# Process Markdown `<pre><code>` blocks.
-#
-
- my $text = shift;
-
- $text =~ s{
- (?:\n\n|\A)
- ( # $1 = the code block -- one or more lines, starting with a space/tab
- (?:
- (?:[ ]{$g_tab_width} | \t) # Lines must start with a tab or a tab-width of spaces
- .*\n+
- )+
- )
- ((?=^[ ]{0,$g_tab_width}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
- }{
- my $codeblock = $1;
- my $result; # return value
-
- $codeblock = _EncodeCode(_Outdent($codeblock));
- $codeblock = _Detab($codeblock);
- $codeblock =~ s/\A\n+//; # trim leading newlines
- $codeblock =~ s/\s+\z//; # trim trailing whitespace
-
- $result = "\n\n<pre><code>" . $codeblock . "\n</code></pre>\n\n";
-
- $result;
- }egmx;
-
- return $text;
-}
-
-
-sub _DoCodeSpans {
-#
-# * Backtick quotes are used for <code></code> spans.
-#
-# * You can use multiple backticks as the delimiters if you want to
-# include literal backticks in the code span. So, this input:
-#
-# Just type ``foo `bar` baz`` at the prompt.
-#
-# Will translate to:
-#
-# <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
-#
-# There's no arbitrary limit to the number of backticks you
-# can use as delimters. If you need three consecutive backticks
-# in your code, use four for delimiters, etc.
-#
-# * You can use spaces to get literal backticks at the edges:
-#
-# ... type `` `bar` `` ...
-#
-# Turns to:
-#
-# ... type <code>`bar`</code> ...
-#
-
- my $text = shift;
-
- $text =~ s@
- (`+) # $1 = Opening run of `
- (.+?) # $2 = The code block
- (?<!`)
- \1 # Matching closer
- (?!`)
- @
- my $c = "$2";
- $c =~ s/^[ \t]*//g; # leading whitespace
- $c =~ s/[ \t]*$//g; # trailing whitespace
- $c = _EncodeCode($c);
- "<code>$c</code>";
- @egsx;
-
- return $text;
-}
-
-
-sub _EncodeCode {
-#
-# Encode/escape certain characters inside Markdown code runs.
-# The point is that in code, these characters are literals,
-# and lose their special Markdown meanings.
-#
- local $_ = shift;
-
- # Encode all ampersands; HTML entities are not
- # entities within a Markdown code span.
- s/&/&/g;
-
- # Encode $'s, but only if we're running under Blosxom.
- # (Blosxom interpolates Perl variables in article bodies.)
- {
- no warnings 'once';
- if (defined($blosxom::version)) {
- s/\$/$/g;
- }
- }
-
-
- # Do the angle bracket song and dance:
- s! < !<!gx;
- s! > !>!gx;
-
- # Now, escape characters that are magic in Markdown:
- s! \* !$g_escape_table{'*'}!gx;
- s! _ !$g_escape_table{'_'}!gx;
- s! { !$g_escape_table{'{'}!gx;
- s! } !$g_escape_table{'}'}!gx;
- s! \[ !$g_escape_table{'['}!gx;
- s! \] !$g_escape_table{']'}!gx;
- s! \\ !$g_escape_table{'\\'}!gx;
-
- return $_;
-}
-
-
-sub _DoItalicsAndBold {
- my $text = shift;
-
- # <strong> must go first:
- $text =~ s{ (\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1 }
- {<strong>$2</strong>}gsx;
-
- $text =~ s{ (\*|_) (?=\S) (.+?) (?<=\S) \1 }
- {<em>$2</em>}gsx;
-
- return $text;
-}
-
-
-sub _DoBlockQuotes {
- my $text = shift;
-
- $text =~ s{
- ( # Wrap whole match in $1
- (
- ^[ \t]*>[ \t]? # '>' at the start of a line
- .+\n # rest of the first line
- (.+\n)* # subsequent consecutive lines
- \n* # blanks
- )+
- )
- }{
- my $bq = $1;
- $bq =~ s/^[ \t]*>[ \t]?//gm; # trim one level of quoting
- $bq =~ s/^[ \t]+$//mg; # trim whitespace-only lines
- $bq = _RunBlockGamut($bq); # recurse
-
- $bq =~ s/^/ /g;
- # These leading spaces screw with <pre> content, so we need to fix that:
- $bq =~ s{
- (\s*<pre>.+?</pre>)
- }{
- my $pre = $1;
- $pre =~ s/^ //mg;
- $pre;
- }egsx;
-
- "<blockquote>\n$bq\n</blockquote>\n\n";
- }egmx;
-
-
- return $text;
-}
-
-
-sub _FormParagraphs {
-#
-# Params:
-# $text - string to process with html <p> tags
-#
- my $text = shift;
-
- # Strip leading and trailing lines:
- $text =~ s/\A\n+//;
- $text =~ s/\n+\z//;
-
- my @grafs = split(/\n{2,}/, $text);
-
- #
- # Wrap <p> tags.
- #
- foreach (@grafs) {
- unless (defined( $g_html_blocks{$_} )) {
- $_ = _RunSpanGamut($_);
- s/^([ \t]*)/<p>/;
- $_ .= "</p>";
- }
- }
-
- #
- # Unhashify HTML blocks
- #
- foreach (@grafs) {
- if (defined( $g_html_blocks{$_} )) {
- $_ = $g_html_blocks{$_};
- }
- }
-
- return join "\n\n", @grafs;
-}
-
-
-sub _EncodeAmpsAndAngles {
-# Smart processing for ampersands and angle brackets that need to be encoded.
-
- my $text = shift;
-
- # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
- # http://bumppo.net/projects/amputator/
- $text =~ s/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/&/g;
-
- # Encode naked <'s
- $text =~ s{<(?![a-z/?\$!])}{<}gi;
-
- return $text;
-}
-
-
-sub _EncodeBackslashEscapes {
-#
-# Parameter: String.
-# Returns: The string, with after processing the following backslash
-# escape sequences.
-#
- local $_ = shift;
-
- s! \\\\ !$g_escape_table{'\\'}!gx; # Must process escaped backslashes first.
- s! \\` !$g_escape_table{'`'}!gx;
- s! \\\* !$g_escape_table{'*'}!gx;
- s! \\_ !$g_escape_table{'_'}!gx;
- s! \\\{ !$g_escape_table{'{'}!gx;
- s! \\\} !$g_escape_table{'}'}!gx;
- s! \\\[ !$g_escape_table{'['}!gx;
- s! \\\] !$g_escape_table{']'}!gx;
- s! \\\( !$g_escape_table{'('}!gx;
- s! \\\) !$g_escape_table{')'}!gx;
- s! \\> !$g_escape_table{'>'}!gx;
- s! \\\# !$g_escape_table{'#'}!gx;
- s! \\\+ !$g_escape_table{'+'}!gx;
- s! \\\- !$g_escape_table{'-'}!gx;
- s! \\\. !$g_escape_table{'.'}!gx;
- s{ \\! }{$g_escape_table{'!'}}gx;
-
- return $_;
-}
-
-
-sub _DoAutoLinks {
- my $text = shift;
-
- $text =~ s{<((https?|ftp):[^'">\s]+)>}{<a href="$1">$1</a>}gi;
-
- # Email addresses: <address@domain.foo>
- $text =~ s{
- <
- (?:mailto:)?
- (
- [-.\w]+
- \@
- [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
- )
- >
- }{
- _EncodeEmailAddress( _UnescapeSpecialChars($1) );
- }egix;
-
- return $text;
-}
-
-
-sub _EncodeEmailAddress {
-#
-# Input: an email address, e.g. "foo@example.com"
-#
-# Output: the email address as a mailto link, with each character
-# of the address encoded as either a decimal or hex entity, in
-# the hopes of foiling most address harvesting spam bots. E.g.:
-#
-# <a href="mailto:foo@e
-# xample.com">foo
-# @example.com</a>
-#
-# Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
-# mailing list: <http://tinyurl.com/yu7ue>
-#
-
- my $addr = shift;
-
- srand;
- my @encode = (
- sub { '&#' . ord(shift) . ';' },
- sub { '&#x' . sprintf( "%X", ord(shift) ) . ';' },
- sub { shift },
- );
-
- $addr = "mailto:" . $addr;
-
- $addr =~ s{(.)}{
- my $char = $1;
- if ( $char eq '@' ) {
- # this *must* be encoded. I insist.
- $char = $encode[int rand 1]->($char);
- } elsif ( $char ne ':' ) {
- # leave ':' alone (to spot mailto: later)
- my $r = rand;
- # roughly 10% raw, 45% hex, 45% dec
- $char = (
- $r > .9 ? $encode[2]->($char) :
- $r < .45 ? $encode[1]->($char) :
- $encode[0]->($char)
- );
- }
- $char;
- }gex;
-
- $addr = qq{<a href="$addr">$addr</a>};
- $addr =~ s{">.+?:}{">}; # strip the mailto: from the visible part
-
- return $addr;
-}
-
-
-sub _UnescapeSpecialChars {
-#
-# Swap back in all the special characters we've hidden.
-#
- my $text = shift;
-
- while( my($char, $hash) = each(%g_escape_table) ) {
- $text =~ s/$hash/$char/g;
- }
- return $text;
-}
-
-
-sub _TokenizeHTML {
-#
-# Parameter: String containing HTML markup.
-# Returns: Reference to an array of the tokens comprising the input
-# string. Each token is either a tag (possibly with nested,
-# tags contained therein, such as <a href="<MTFoo>">, or a
-# run of text between tags. Each element of the array is a
-# two-element array; the first is either 'tag' or 'text';
-# the second is the actual value.
-#
-#
-# Derived from the _tokenize() subroutine from Brad Choate's MTRegex plugin.
-# <http://www.bradchoate.com/past/mtregex.php>
-#
-
- my $str = shift;
- my $pos = 0;
- my $len = length $str;
- my @tokens;
-
- my $depth = 6;
- my $nested_tags = join('|', ('(?:<[a-z/!$](?:[^<>]') x $depth) . (')*>)' x $depth);
- my $match = qr/(?s: <! ( -- .*? -- \s* )+ > ) | # comment
- (?s: <\? .*? \?> ) | # processing instruction
- $nested_tags/ix; # nested tags
-
- while ($str =~ m/($match)/g) {
- my $whole_tag = $1;
- my $sec_start = pos $str;
- my $tag_start = $sec_start - length $whole_tag;
- if ($pos < $tag_start) {
- push @tokens, ['text', substr($str, $pos, $tag_start - $pos)];
- }
- push @tokens, ['tag', $whole_tag];
- $pos = pos $str;
- }
- push @tokens, ['text', substr($str, $pos, $len - $pos)] if $pos < $len;
- \@tokens;
-}
-
-
-sub _Outdent {
-#
-# Remove one level of line-leading tabs or spaces
-#
- my $text = shift;
-
- $text =~ s/^(\t|[ ]{1,$g_tab_width})//gm;
- return $text;
-}
-
-
-sub _Detab {
-#
-# Cribbed from a post by Bart Lateur:
-# <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
-#
- my $text = shift;
-
- $text =~ s{(.*?)\t}{$1.(' ' x ($g_tab_width - length($1) % $g_tab_width))}ge;
- return $text;
-}
-
-
-1;
-
-__END__
-
-
-=pod
-
-=head1 NAME
-
-B<Markdown>
-
-
-=head1 SYNOPSIS
-
-B<Markdown.pl> [ B<--html4tags> ] [ B<--version> ] [ B<-shortversion> ]
- [ I<file> ... ]
-
-
-=head1 DESCRIPTION
-
-Markdown is a text-to-HTML filter; it translates an easy-to-read /
-easy-to-write structured text format into HTML. Markdown's text format
-is most similar to that of plain text email, and supports features such
-as headers, *emphasis*, code blocks, blockquotes, and links.
-
-Markdown's syntax is designed not as a generic markup language, but
-specifically to serve as a front-end to (X)HTML. You can use span-level
-HTML tags anywhere in a Markdown document, and you can use block level
-HTML tags (like <div> and <table> as well).
-
-For more information about Markdown's syntax, see:
-
- http://daringfireball.net/projects/markdown/
-
-
-=head1 OPTIONS
-
-Use "--" to end switch parsing. For example, to open a file named "-z", use:
-
- Markdown.pl -- -z
-
-=over 4
-
-
-=item B<--html4tags>
-
-Use HTML 4 style for empty element tags, e.g.:
-
- <br>
-
-instead of Markdown's default XHTML style tags, e.g.:
-
- <br />
-
-
-=item B<-v>, B<--version>
-
-Display Markdown's version number and copyright information.
-
-
-=item B<-s>, B<--shortversion>
-
-Display the short-form version number.
-
-
-=back
-
-
-
-=head1 BUGS
-
-To file bug reports or feature requests (other than topics listed in the
-Caveats section above) please send email to:
-
- support@daringfireball.net
-
-Please include with your report: (1) the example input; (2) the output
-you expected; (3) the output Markdown actually produced.
-
-
-=head1 VERSION HISTORY
-
-See the readme file for detailed release notes for this version.
-
-1.0.1 - 14 Dec 2004
-
-1.0 - 28 Aug 2004
-
-
-=head1 AUTHOR
-
- John Gruber
- http://daringfireball.net
-
- PHP port and other contributions by Michel Fortin
- http://michelf.com
-
-
-=head1 COPYRIGHT AND LICENSE
-
-Copyright (c) 2003-2004 John Gruber
-<http://daringfireball.net/>
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-* Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
-* Neither the name "Markdown" nor the names of its contributors may
- be used to endorse or promote products derived from this software
- without specific prior written permission.
-
-This software is provided by the copyright holders and contributors "as
-is" and any express or implied warranties, including, but not limited
-to, the implied warranties of merchantability and fitness for a
-particular purpose are disclaimed. In no event shall the copyright owner
-or contributors be liable for any direct, indirect, incidental, special,
-exemplary, or consequential damages (including, but not limited to,
-procurement of substitute goods or services; loss of use, data, or
-profits; or business interruption) however caused and on any theory of
-liability, whether in contract, strict liability, or tort (including
-negligence or otherwise) arising in any way out of the use of this
-software, even if advised of the possibility of such damage.
-
-=cut
--- /dev/null
+#!/bin/bash
+#
+# Mo is a mustache template rendering software written in bash. It inserts
+# environment variables into templates.
+#
+# Learn more about mustache templates at https://mustache.github.io/
+#
+# Mo is under a MIT style licence with an additional non-advertising clause.
+# See LICENSE.md for the full text.
+#
+# This is open source! Please feel free to contribute.
+#
+# https://github.com/tests-always-included/mo
+
+
+# Scan content until the right end tag is found. Returns an array with the
+# following members:
+# [0] = Content before end tag
+# [1] = End tag (complete tag)
+# [2] = Content after end tag
+#
+# Everything using this function uses the "standalone tags" logic.
+#
+# Parameters:
+# $1: Where to store the array
+# $2: Content
+# $3: Name of end tag
+# $4: If -z, do standalone tag processing before finishing
+mustache-find-end-tag() {
+ local CONTENT SCANNED
+
+ # Find open tags
+ SCANNED=""
+ mustache-split CONTENT "$2" '{{' '}}'
+
+ while [[ "${#CONTENT[@]}" -gt 1 ]]; do
+ mustache-trim-whitespace TAG "${CONTENT[1]}"
+
+ # Restore CONTENT[1] before we start using it
+ CONTENT[1]='{{'"${CONTENT[1]}"'}}'
+
+ case $TAG in
+ '#'* | '^'*)
+ # Start another block
+ SCANNED="${SCANNED}${CONTENT[0]}${CONTENT[1]}"
+ mustache-trim-whitespace TAG "${TAG:1}"
+ mustache-find-end-tag CONTENT "${CONTENT[2]}" "$TAG" "loop"
+ SCANNED="${SCANNED}${CONTENT[0]}${CONTENT[1]}"
+ CONTENT=${CONTENT[2]}
+ ;;
+
+ '/'*)
+ # End a block - could be ours
+ mustache-trim-whitespace TAG "${TAG:1}"
+ SCANNED="$SCANNED${CONTENT[0]}"
+
+ if [[ "$TAG" == "$3" ]]; then
+ # Found our end tag
+ if [[ -z "$4" ]] && mustache-is-standalone STANDALONE_BYTES "$SCANNED" "${CONTENT[2]}" true; then
+ # This is also a standalone tag - clean up whitespace
+ # and move those whitespace bytes to the "tag" element
+ STANDALONE_BYTES=( $STANDALONE_BYTES )
+ CONTENT[1]="${SCANNED:${STANDALONE_BYTES[0]}}${CONTENT[1]}${CONTENT[2]:0:${STANDALONE_BYTES[1]}}"
+ SCANNED="${SCANNED:0:${STANDALONE_BYTES[0]}}"
+ CONTENT[2]="${CONTENT[2]:${STANDALONE_BYTES[1]}}"
+ fi
+
+ local "$1" && mustache-indirect-array "$1" "$SCANNED" "${CONTENT[1]}" "${CONTENT[2]}"
+ return 0
+ fi
+
+ SCANNED="$SCANNED${CONTENT[1]}"
+ CONTENT=${CONTENT[2]}
+ ;;
+
+ *)
+ # Ignore all other tags
+ SCANNED="${SCANNED}${CONTENT[0]}${CONTENT[1]}"
+ CONTENT=${CONTENT[2]}
+ ;;
+ esac
+
+ mustache-split CONTENT "$CONTENT" '{{' '}}'
+ done
+
+ # Did not find our closing tag
+ SCANNED="$SCANNED${CONTENT[0]}"
+ local "$1" && mustache-indirect-array "$1" "${SCANNED}" "" ""
+}
+
+
+# Find the first index of a substring
+#
+# Parameters:
+# $1: Destination variable
+# $2: Haystack
+# $3: Needle
+mustache-find-string() {
+ local POS STRING
+
+ STRING=${2%%$3*}
+ [[ "$STRING" == "$2" ]] && POS=-1 || POS=${#STRING}
+ local "$1" && mustache-indirect "$1" $POS
+}
+
+
+# Return a dotted name based on current context and target name
+#
+# Parameters:
+# $1: Target variable to store results
+# $2: Context name
+# $3: Desired variable name
+mustache-full-tag-name() {
+ if [[ -z "$2" ]]; then
+ local "$1" && mustache-indirect "$1" "$3"
+ else
+ local "$1" && mustache-indirect "$1" "${2}.${3}"
+ fi
+}
+
+
+# Return the content to parse. Can be a list of partials for files or
+# the content from stdin.
+#
+# Parameters:
+# $1: Variable name to assign this content back as
+# $2-*: File names (optional)
+mustache-get-content() {
+ local CONTENT FILENAME TARGET
+
+ TARGET=$1
+ shift
+ if [[ "${#@}" -gt 0 ]]; then
+ CONTENT=""
+
+ for FILENAME in "$@"; do
+ # This is so relative paths work from inside template files
+ CONTENT="$CONTENT"'{{>'"$FILENAME"'}}'
+ done
+ else
+ mustache-load-file CONTENT /dev/stdin
+ fi
+
+ local "$TARGET" && mustache-indirect "$TARGET" "$CONTENT"
+}
+
+
+# Indent a string, placing the indent at the beginning of every
+# line that has any content.
+#
+# Parameters:
+# $1: Name of destination variable to get an array of lines
+# $2: The indent string
+# $3: The string to reindent
+mustache-indent-lines() {
+ local CONTENT FRAGMENT LEN POS_N POS_R RESULT TRIMMED
+
+ RESULT=""
+ LEN=$((${#3} - 1))
+ CONTENT="${3:0:$LEN}" # Remove newline and dot from workaround - in mustache-partial
+
+ if [ -z "$2" ]; then
+ local "$1" && mustache-indirect "$1" "$CONTENT"
+ return 0
+ fi
+
+ mustache-find-string POS_N "$CONTENT" $'\n'
+ mustache-find-string POS_R "$CONTENT" $'\r'
+
+ while [[ "$POS_N" -gt -1 ]] || [[ "$POS_R" -gt -1 ]]; do
+ if [[ "$POS_N" -gt -1 ]]; then
+ FRAGMENT="${CONTENT:0:$POS_N + 1}"
+ CONTENT=${CONTENT:$POS_N + 1}
+ else
+ FRAGMENT="${CONTENT:0:$POS_R + 1}"
+ CONTENT=${CONTENT:$POS_R + 1}
+ fi
+
+ mustache-trim-chars TRIMMED "$FRAGMENT" false true " " $'\t' $'\n' $'\r'
+
+ if [ ! -z "$TRIMMED" ]; then
+ FRAGMENT="$2$FRAGMENT"
+ fi
+
+ RESULT="$RESULT$FRAGMENT"
+ mustache-find-string POS_N "$CONTENT" $'\n'
+ mustache-find-string POS_R "$CONTENT" $'\r'
+ done
+
+ mustache-trim-chars TRIMMED "$CONTENT" false true " " $'\t'
+
+ if [ ! -z "$TRIMMED" ]; then
+ CONTENT="$2$CONTENT"
+ fi
+
+ RESULT="$RESULT$CONTENT"
+
+ local "$1" && mustache-indirect "$1" "$RESULT"
+}
+
+
+# Send a variable up to caller of a function
+#
+# Parameters:
+# $1: Variable name
+# $2: Value
+mustache-indirect() {
+ unset -v "$1"
+ printf -v "$1" '%s' "$2"
+}
+
+
+# Send an array up to caller of a function
+#
+# Parameters:
+# $1: Variable name
+# $2-*: Array elements
+mustache-indirect-array() {
+ unset -v "$1"
+ eval $1=\(\"\${@:2}\"\)
+}
+
+
+# Determine if a given environment variable exists and if it is an array.
+#
+# Parameters:
+# $1: Name of environment variable
+#
+# Return code:
+# 0 if the name is not empty, 1 otherwise
+mustache-is-array() {
+ local MUSTACHE_TEST
+
+ MUSTACHE_TEST=$(declare -p "$1" 2>/dev/null) || return 1
+ [[ "${MUSTACHE_TEST:0:10}" == "declare -a" ]] && return 0
+ [[ "${MUSTACHE_TEST:0:10}" == "declare -A" ]] && return 0
+
+ return 1
+}
+
+
+# Return 0 if the passed name is a function.
+#
+# Parameters:
+# $1: Name to check if it's a function
+#
+# Return code:
+# 0 if the name is a function, 1 otherwise
+mustache-is-function() {
+ local FUNCTIONS NAME
+
+ FUNCTIONS=$(declare -F)
+ FUNCTIONS=( ${FUNCTIONS//declare -f /} )
+
+ for NAME in ${FUNCTIONS[@]}; do
+ if [[ "$NAME" == "$1" ]]; then
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+
+# Determine if the tag is a standalone tag based on whitespace before and
+# after the tag.
+#
+# Passes back a string containing two numbers in the format "BEFORE AFTER"
+# like "27 10". It indicates the number of bytes remaining in the "before"
+# string (27) and the number of bytes to trim in the "after" string (10).
+# Useful for string manipulation:
+#
+# mustache-is-standalone RESULT "$before" "$after" false || return 0
+# RESULT_ARRAY=( $RESULT )
+# echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}"
+#
+# Parameters:
+# $1: Variable to pass data back
+# $2: Content before the tag
+# $3: Content after the tag
+# $4: true/false: is this the beginning of the content?
+mustache-is-standalone() {
+ local AFTER_TRIMMED BEFORE_TRIMMED CHAR
+
+ mustache-trim-chars BEFORE_TRIMMED "$2" false true " " $'\t'
+ mustache-trim-chars AFTER_TRIMMED "$3" true false " " $'\t'
+ CHAR=$((${#BEFORE_TRIMMED} - 1))
+ CHAR=${BEFORE_TRIMMED:$CHAR}
+
+ if [[ "$CHAR" != $'\n' ]] && [[ "$CHAR" != $'\r' ]]; then
+ if [[ ! -z "$CHAR" ]] || ! $4; then
+ return 1;
+ fi
+ fi
+
+ CHAR=${AFTER_TRIMMED:0:1}
+
+ if [[ "$CHAR" != $'\n' ]] && [[ "$CHAR" != $'\r' ]] && [[ ! -z "$CHAR" ]]; then
+ return 2;
+ fi
+
+ if [[ "$CHAR" == $'\r' ]] && [[ "${AFTER_TRIMMED:1:1}" == $'\n' ]]; then
+ CHAR="$CHAR"$'\n'
+ fi
+
+ local "$1" && mustache-indirect "$1" "$((${#BEFORE_TRIMMED})) $((${#3} + ${#CHAR} - ${#AFTER_TRIMMED}))"
+}
+
+
+# Join / implode an array
+#
+# Parameters:
+# $1: Variable name to receive the joined content
+# $2: Joiner
+# $3-$*: Elements to join
+mustache-join() {
+ local JOINER PART RESULT TARGET
+
+ TARGET=$1
+ JOINER=$2
+ RESULT=$3
+ shift 3
+
+ for PART in "$@"; do
+ RESULT="$RESULT$JOINER$PART"
+ done
+
+ local "$TARGET" && mustache-indirect "$TARGET" "$RESULT"
+}
+
+# Read a file
+#
+# Parameters:
+# $1: Variable name to receive the file's content
+# $2: Filename to load
+mustache-load-file() {
+ local CONTENT LEN
+
+ # The subshell removes any trailing newlines. We forcibly add
+ # a dot to the content to preserve all newlines.
+ # TODO: remove cat and replace with read loop?
+ CONTENT=$(cat $2; echo '.')
+ LEN=$((${#CONTENT} - 1))
+ CONTENT=${CONTENT:0:$LEN} # Remove last dot
+
+ local "$1" && mustache-indirect "$1" "$CONTENT"
+}
+
+
+# Process a chunk of content some number of times.
+#
+# Parameters:
+# $1: Content to parse and reparse and reparse
+# $2: Tag prefix (context name)
+# $3-*: Names to insert into the parsed content
+mustache-loop() {
+ local CONTENT CONTEXT CONTEXT_BASE IGNORE
+
+ CONTENT=$1
+ CONTEXT_BASE=$2
+ shift 2
+
+ while [[ "${#@}" -gt 0 ]]; do
+ mustache-full-tag-name CONTEXT "$CONTEXT_BASE" "$1"
+ mustache-parse "$CONTENT" "$CONTEXT" false
+ shift
+ done
+}
+
+
+# Parse a block of text
+#
+# Parameters:
+# $1: Block of text to change
+# $2: Current name (the variable NAME for what {{.}} means)
+# $3: true when no content before this, false otherwise
+mustache-parse() {
+ # Keep naming variables MUSTACHE_* here to not overwrite needed variables
+ # used in the string replacements
+ local MUSTACHE_BLOCK MUSTACHE_CONTENT MUSTACHE_CURRENT MUSTACHE_IS_BEGINNING MUSTACHE_TAG
+
+ MUSTACHE_CURRENT=$2
+ MUSTACHE_IS_BEGINNING=$3
+
+ # Find open tags
+ mustache-split MUSTACHE_CONTENT "$1" '{{' '}}'
+
+ while [[ "${#MUSTACHE_CONTENT[@]}" -gt 1 ]]; do
+ mustache-trim-whitespace MUSTACHE_TAG "${MUSTACHE_CONTENT[1]}"
+
+ case $MUSTACHE_TAG in
+ '#'*)
+ # Loop, if/then, or pass content through function
+ # Sets context
+ mustache-standalone-allowed MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}" $MUSTACHE_IS_BEGINNING
+ mustache-trim-whitespace MUSTACHE_TAG "${MUSTACHE_TAG:1}"
+ mustache-find-end-tag MUSTACHE_BLOCK "$MUSTACHE_CONTENT" "$MUSTACHE_TAG"
+ mustache-full-tag-name MUSTACHE_TAG "$MUSTACHE_CURRENT" "$MUSTACHE_TAG"
+
+ if mustache-test "$MUSTACHE_TAG"; then
+ # Show / loop / pass through function
+ if mustache-is-function "$MUSTACHE_TAG"; then
+ # TODO: Consider piping the output to
+ # mustache-get-content so the lambda does not
+ # execute in a subshell?
+ MUSTACHE_CONTENT=$($MUSTACHE_TAG "${MUSTACHE_BLOCK[0]}")
+ mustache-parse "$MUSTACHE_CONTENT" "$MUSTACHE_CURRENT" false
+ MUSTACHE_CONTENT="${MUSTACHE_BLOCK[2]}"
+ elif mustache-is-array "$MUSTACHE_TAG"; then
+ eval 'mustache-loop "${MUSTACHE_BLOCK[0]}" "$MUSTACHE_TAG" "${!'"$MUSTACHE_TAG"'[@]}"'
+ else
+ mustache-parse "${MUSTACHE_BLOCK[0]}" "$MUSTACHE_CURRENT" false
+ fi
+ fi
+
+ MUSTACHE_CONTENT="${MUSTACHE_BLOCK[2]}"
+ ;;
+
+ '>'*)
+ # Load partial - get name of file relative to cwd
+ mustache-partial MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}" $MUSTACHE_IS_BEGINNING "$MUSTACHE_CURRENT"
+ ;;
+
+ '/'*)
+ # Closing tag - If hit in this loop, we simply ignore
+ # Matching tags are found in mustache-find-end-tag
+ mustache-standalone-allowed MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}" $MUSTACHE_IS_BEGINNING
+ ;;
+
+ '^'*)
+ # Display section if named thing does not exist
+ mustache-standalone-allowed MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}" $MUSTACHE_IS_BEGINNING
+ mustache-trim-whitespace MUSTACHE_TAG "${MUSTACHE_TAG:1}"
+ mustache-find-end-tag MUSTACHE_BLOCK "$MUSTACHE_CONTENT" "$MUSTACHE_TAG"
+ mustache-full-tag-name MUSTACHE_TAG "$MUSTACHE_CURRENT" "$MUSTACHE_TAG"
+
+ if ! mustache-test "$MUSTACHE_TAG"; then
+ mustache-parse "${MUSTACHE_BLOCK[0]}" "$MUSTACHE_CURRENT" false "$MUSTACHE_CURRENT"
+ fi
+
+ MUSTACHE_CONTENT="${MUSTACHE_BLOCK[2]}"
+ ;;
+
+ '!'*)
+ # Comment - ignore the tag content entirely
+ # Trim spaces/tabs before the comment
+ mustache-standalone-allowed MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}" $MUSTACHE_IS_BEGINNING
+ ;;
+
+ .)
+ # Current content (environment variable or function)
+ mustache-standalone-denied MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}"
+ mustache-show "$MUSTACHE_CURRENT" "$MUSTACHE_CURRENT"
+ ;;
+
+ '=')
+ # Change delimiters
+ # Any two non-whitespace sequences separated by whitespace.
+ # TODO
+ mustache-standalone-allowed MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}" $MUSTACHE_IS_BEGINNING
+ ;;
+
+ '{'*)
+ # Unescaped - split on }}} not }}
+ mustache-standalone-denied MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}"
+ MUSTACHE_CONTENT="${MUSTACHE_TAG:1}"'}}'"$MUSTACHE_CONTENT"
+ mustache-split MUSTACHE_CONTENT "$MUSTACHE_CONTENT" '}}}'
+ mustache-trim-whitespace MUSTACHE_TAG "${MUSTACHE_CONTENT[0]}"
+ mustache-full-tag-name MUSTACHE_TAG "$MUSTACHE_CURRENT" "$MUSTACHE_TAG"
+ MUSTACHE_CONTENT=${MUSTACHE_CONTENT[1]}
+
+ # Now show the value
+ mustache-show "$MUSTACHE_TAG" "$MUSTACHE_CURRENT"
+ ;;
+
+ '&'*)
+ # Unescaped
+ mustache-standalone-denied MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}"
+ mustache-trim-whitespace MUSTACHE_TAG "${MUSTACHE_TAG:1}"
+ mustache-full-tag-name MUSTACHE_TAG "$MUSTACHE_CURRENT" "$MUSTACHE_TAG"
+ mustache-show "$MUSTACHE_TAG" "$MUSTACHE_CURRENT"
+ ;;
+
+ *)
+ # Normal environment variable or function call
+ mustache-standalone-denied MUSTACHE_CONTENT "${MUSTACHE_CONTENT[@]}"
+ mustache-full-tag-name MUSTACHE_TAG "$MUSTACHE_CURRENT" "$MUSTACHE_TAG"
+ mustache-show "$MUSTACHE_TAG" "$MUSTACHE_CURRENT"
+ ;;
+ esac
+
+ MUSTACHE_IS_BEGINNING=false
+ mustache-split MUSTACHE_CONTENT "$MUSTACHE_CONTENT" '{{' '}}'
+ done
+
+ echo -n "${MUSTACHE_CONTENT[0]}"
+}
+
+
+# Process a partial
+#
+# Indentation should be applied to the entire partial
+#
+# Prefix all variables
+#
+# Parameters:
+# $1: Name of destination "content" variable.
+# $2: Content before the tag that was not yet written
+# $3: Tag content
+# $4: Content after the tag
+# $5: true/false: is this the beginning of the content?
+# $6: Current context name
+mustache-partial() {
+ local MUSTACHE_CONTENT MUSTACHE_FILENAME MUSTACHE_INDENT MUSTACHE_LINE MUSTACHE_PARTIAL MUSTACHE_STANDALONE
+
+ if mustache-is-standalone MUSTACHE_STANDALONE "$2" "$4" $5; then
+ MUSTACHE_STANDALONE=( $MUSTACHE_STANDALONE )
+ echo -n "${2:0:${MUSTACHE_STANDALONE[0]}}"
+ MUSTACHE_INDENT=${2:${MUSTACHE_STANDALONE[0]}}
+ MUSTACHE_CONTENT=${4:${MUSTACHE_STANDALONE[1]}}
+ else
+ MUSTACHE_INDENT=""
+ echo -n "$2"
+ MUSTACHE_CONTENT=$4
+ fi
+
+ mustache-trim-whitespace MUSTACHE_FILENAME "${3:1}"
+
+ # Execute in subshell to preserve current cwd and environment
+ (
+ # TODO: Remove dirname and use a function instead
+ cd "$(dirname "$MUSTACHE_FILENAME")"
+ mustache-indent-lines MUSTACHE_PARTIAL "$MUSTACHE_INDENT" "$(
+ mustache-load-file MUSTACHE_PARTIAL "${MUSTACHE_FILENAME##*/}"
+
+ # Fix bash handling of subshells
+ # The extra dot is removed in mustache-indent-lines
+ echo -n "${MUSTACHE_PARTIAL}."
+ )"
+ mustache-parse "$MUSTACHE_PARTIAL" "$6" true
+ )
+
+ local "$1" && mustache-indirect "$1" "$MUSTACHE_CONTENT"
+}
+
+
+# Show an environment variable or the output of a function.
+#
+# Limit/prefix any variables used
+#
+# Parameters:
+# $1: Name of environment variable or function
+# $2: Current context
+mustache-show() {
+ local JOINED MUSTACHE_NAME_PARTS
+
+ if mustache-is-function "$1"; then
+ CONTENT=$($1 "")
+ mustache-parse "$CONTENT" "$2" false
+ return 0
+ fi
+
+ mustache-split MUSTACHE_NAME_PARTS "$1" "."
+
+ if [[ -z "${MUSTACHE_NAME_PARTS[1]}" ]]; then
+ if mustache-is-array "$1"; then
+ eval mustache-join JOINED "," "\${$1[@]}"
+ echo -n "$JOINED"
+ else
+ echo -n "${!1}"
+ fi
+ else
+ # Further subindexes are disallowed
+ eval 'echo -n "${'"${MUSTACHE_NAME_PARTS[0]}"'['"${MUSTACHE_NAME_PARTS[1]%%.*}"']}"'
+ fi
+}
+
+
+# Split a larger string into an array
+#
+# Parameters:
+# $1: Destination variable
+# $2: String to split
+# $3: Starting delimiter
+# $4: Ending delimiter (optional)
+mustache-split() {
+ local POS RESULT
+
+ RESULT=( "$2" )
+ mustache-find-string POS "${RESULT[0]}" "$3"
+
+ if [[ "$POS" -ne -1 ]]; then
+ # The first delimiter was found
+ RESULT[1]=${RESULT[0]:$POS + ${#3}}
+ RESULT[0]=${RESULT[0]:0:$POS}
+
+ if [[ ! -z "$4" ]]; then
+ mustache-find-string POS "${RESULT[1]}" "$4"
+
+ if [[ "$POS" -ne -1 ]]; then
+ # The second delimiter was found
+ RESULT[2]="${RESULT[1]:$POS + ${#4}}"
+ RESULT[1]="${RESULT[1]:0:$POS}"
+ fi
+ fi
+ fi
+
+ local "$1" && mustache-indirect-array "$1" "${RESULT[@]}"
+}
+
+
+# Handle the content for a standalone tag. This means removing whitespace
+# (not newlines) before a tag and whitespace and a newline after a tag.
+# That is, assuming, that the line is otherwise empty.
+#
+# Parameters:
+# $1: Name of destination "content" variable.
+# $2: Content before the tag that was not yet written
+# $3: Tag content (not used)
+# $4: Content after the tag
+# $5: true/false: is this the beginning of the content?
+mustache-standalone-allowed() {
+ local STANDALONE_BYTES
+
+ if mustache-is-standalone STANDALONE_BYTES "$2" "$4" $5; then
+ STANDALONE_BYTES=( $STANDALONE_BYTES )
+ echo -n "${2:0:${STANDALONE_BYTES[0]}}"
+ local "$1" && mustache-indirect "$1" "${4:${STANDALONE_BYTES[1]}}"
+ else
+ echo -n "$2"
+ local "$1" && mustache-indirect "$1" "$4"
+ fi
+}
+
+
+# Handle the content for a tag that is never "standalone". No adjustments
+# are made for newlines and whitespace.
+#
+# Parameters:
+# $1: Name of destination "content" variable.
+# $2: Content before the tag that was not yet written
+# $3: Tag content (not used)
+# $4: Content after the tag
+mustache-standalone-denied() {
+ echo -n "$2"
+ local "$1" && mustache-indirect "$1" "$4"
+}
+
+
+# Returns 0 (success) if the named thing is a function or if it is a non-empty
+# environment variable.
+#
+# Do not use unprefixed variables here if possible as this needs to check
+# if any name exists in the environment
+#
+# Parameters:
+# $1: Name of environment variable or function
+# $2: Current value (our context)
+#
+# Return code:
+# 0 if the name is not empty, 1 otherwise
+mustache-test() {
+ # Test for functions
+ mustache-is-function "$1" && return 0
+
+ if mustache-is-array "$1"; then
+ # Arrays must have at least 1 element
+ eval '[[ "${#'"$1"'[@]}" -gt 0 ]]' && return 0
+ else
+ # Environment variables must not be empty
+ [[ ! -z "${!1}" ]] && return 0
+ fi
+
+ return 1
+}
+
+
+# Trim the leading whitespace only
+#
+# Parameters:
+# $1: Name of destination variable
+# $2: The string
+# $3: true/false - trim front?
+# $4: true/false - trim end?
+# $5-*: Characters to trim
+mustache-trim-chars() {
+ local BACK CURRENT FRONT LAST TARGET VAR
+
+ TARGET=$1
+ CURRENT=$2
+ FRONT=$3
+ BACK=$4
+ LAST=""
+ shift # Remove target
+ shift # Remove string
+ shift # Remove trim front flag
+ shift # Remove trim end flag
+
+ while [[ "$CURRENT" != "$LAST" ]]; do
+ LAST=$CURRENT
+
+ for VAR in "$@"; do
+ $FRONT && CURRENT="${CURRENT/#$VAR}"
+ $BACK && CURRENT="${CURRENT/%$VAR}"
+ done
+ done
+
+ local "$TARGET" && mustache-indirect "$TARGET" "$CURRENT"
+}
+
+
+# Trim leading and trailing whitespace from a string
+#
+# Parameters:
+# $1: Name of variable to store trimmed string
+# $2: The string
+mustache-trim-whitespace() {
+ local RESULT
+
+ mustache-trim-chars RESULT "$2" true true $'\r' $'\n' $'\t' " "
+ local "$1" && mustache-indirect "$1" "$RESULT"
+}
+
+
+mustache-get-content MUSTACHE_CONTENT "$@"
+mustache-parse "$MUSTACHE_CONTENT" "" true