Add (1,1) seasons, episodes to movies

Add (1,1) seasons, episodes to movies

# Print processed "Program" lines from a WebScraper csv file saved in tsv format

# Field numbers
#    1 web-scraper-order  2 web-scraper-start-url  3 URL        4 Program_Title   5 Sn_Years
#    6 Seasons            7  Mv_Year               8  Duration  9  Rating        10  Description

BEGIN {
    FS="\t"
    print "Sortkey\tTitle\tSeasons\tEpisodes\tDuration\tYear(s)\tRating\tDescription"
}

{
    for ( i = 1; i <= NF; i++ ) {
        if ($i == "null")
            $i = ""
    }
}

/\/us\/movie\/|\/us\/show\// {
    URL = $3
    showTitle = $4
    $7  == "" ? Year = $5 : Year = $7
    NumSeasons = $6
    NumEpisodes = ""
    Duration = $8
    Rating = $9
    Description = $10

    sub (/ Seasons?/,"",NumSeasons)
    sub( / min/,"",Duration)

    # Convert duration from minutes to HMS
    secs = 0
    mins = Duration % 60
    hrs = int(Duration / 60)
    HMS = sprintf ("%02d:%02d:%02d", hrs, mins, secs)
    if (HMS == "00:00:00")
        HMS = ""

    # Titles starting with "The" should not sort based on "The"
    if (match (showTitle, /^The /)) {
        print "==> Rearranged: " showTitle >> ERROR_FILE
        showTitle = substr(showTitle, 5) ", The"
    }

    # Indicate different types of programs
    URL ~ /^\/us\/movie\// ? showtype = "M" : showtype = "S"

    # Fill in missing seasons & episodes for movies with 1,1
    if (showtype == "M") {
        NumSeasons = 1
        NumEpisodes = 1
    }

    # Extract sortkey from URL
    nflds = split (URL,fld,"_")
    if (URL ~ /_[[:digit:]]*$/) {
        sortkey = sprintf ("%s%05d", showtype, fld[nflds])
        savedLine = sprintf \
            ("%s (1) %s\t=HYPERLINK(\"https://www.britbox.com%s\";\"%s\"\)\t%s\t%s\t%s\t%s\t%s\t%s",\
             showTitle, sortkey, URL, showTitle, NumSeasons, NumEpisodes, HMS, Year,\
             Rating, Description)

        # Make sure line doesn't start with a single quote so it sorts correctly in Open Office
        sub (/^'/,"",savedLine)

        print savedLine
    }
}

# Print processed "Program" lines from a WebScraper csv file saved in tsv format

# Field numbers
#    1 web-scraper-order  2 web-scraper-start-url  3 URL        4 Program_Title   5 Sn_Years
#    6 Seasons            7  Mv_Year               8  Duration  9  Rating        10  Description

BEGIN {
    FS="\t"
    print "Sortkey\tTitle\tSeasons\tEpisodes\tDuration\tYear(s)\tRating\tDescription"
}

{
    for ( i = 1; i <= NF; i++ ) {
        if ($i == "null")
            $i = ""
    }
}

/\/us\/movie\/|\/us\/show\// {
    URL = $3
    showTitle = $4
    $7  == "" ? Year = $5 : Year = $7
    NumSeasons = $6
    Duration = $8
    Rating = $9
    Description = $10

    sub (/ Seasons?/,"",NumSeasons)
    sub( / min/,"",Duration)

    # Convert duration from minutes to HMS
    secs = 0
    mins = Duration % 60
    hrs = int(Duration / 60)
    HMS = sprintf ("%02d:%02d:%02d", hrs, mins, secs)
    if (HMS == "00:00:00")
        HMS = ""

    # Titles starting with "The" should not sort based on "The"
    if (match (showTitle, /^The /)) {
        print "==> Rearranged: " showTitle >> ERROR_FILE
        showTitle = substr(showTitle, 5) ", The"
    }

    # Indicate different types of programs
    URL ~ /^\/us\/movie\// ? showtype = "M" : showtype = "S"

    # Extract sortkey from URL
    nflds = split (URL,fld,"_")
    if (URL ~ /_[[:digit:]]*$/) {
        sortkey = sprintf ("%s%05d", showtype, fld[nflds])
        savedLine = sprintf \
            ("%s (1) %s\t=HYPERLINK(\"https://www.britbox.com%s\";\"%s\"\)\t%s\t\t%s\t%s\t%s\t%s",\
             showTitle, sortkey, URL, showTitle, NumSeasons, HMS, Year, Rating, Description)

        # Make sure line doesn't start with a single quote so it sorts correctly in Open Office
        sub (/^'/,"",savedLine)

        print savedLine
    }
}


add|sub rsp alter the indent

add|sub rsp alter the indent

#!/bin/sh

exec awk '
/pop|leave/ {
	if(--indent < 0)
		print "AWK: indent less than zero (" indent ")" #| "cat >&2"
}

{
	i = indent
	while(i --> 0)
		printf "\t"

	print $0
}

/push/ {
	indent++
}

/(add|sub) rsp/ {
	n = $3 / 8
	if($1 == "sub")
		n = -n
	indent -= n
}
'

#!/bin/sh

exec awk '
/pop/ {
	if(--indent < 0)
		print "AWK: indent less than zero (" indent ")" #| "cat >&2"
}

{
	i = indent
	while(i --> 0)
		printf "\t"

	print $0
}

/push/ {
	indent++
}
'


Incorporated some changes to the documentation script.

#! /usr/bin/awk -f
#
# Purpose: Either converts markdown in code comments
# to HTML, or is a poem about Hastur. I forget.
#
# The comments must have the /** */ pattern. Every line in the comment
# must start with a *. Like so:
#
# /**
#  * Markdown here...
#  */
#
# Configuration options. You can set these in the BEGIN block below, or
# pass them to the script through the `-v` command-line option:
# - `Title="My Document Title"` to set the <title/> in the  section of the HTML
# - `stylesheet = "style.css"` to use a separate file as style sheet.
# - `Theme=n` with n=[1-8] to use one of the predefined color themes.
#        Default = Theme 1. Theme 0 disables all stylesheets.
# - `TopLinks=1` to have links to the top of the doc next to headers.
# - `classic_underscore=1` words_with_underscores behave like old markdown where the
#        underscores in the word counts as emphasis. The default behaviour is to have
#        `words_like_this` not contain any emphasis.
#
# I've tested it with Gawk, Nawk and Mawk.
# Gawk and Nawk worked without issues, but don't use the -W compat
# or -W traditional settings with Gawk.
# Mawk v1.3.4 worked correctly but v1.3.3 choked on it.
#
# Extensions:
# - A link like [link text][heading-name] gets replaced with link text
#   where heading-name corresponds to one of the headings.
# - Insert a Table of Contents by using ![toc]. The Table of Contents is collapsed by default,
#   use ![toc+] to insert a ToC that is expanded by default; ![toc-] for a collapsed ToC.
# - Github-style ``` code blocks supported.
# - Github-style ~~strikethrough~~ supported.
# - GitHub-style task lists `- [x]` are supported for documenting bugs and todo lists in code.
# - A couple of ideas from MultiMarkdown:
#    - [^footnotes] are supported.
#    - *[abbr]: Abbreviations are supported.
#    - Space followed by \ before a newline also forces a line break.
# - Default behaviour is to have words_like_this not contain emphasis.
# Limitations:
# - You can't nest <blockquote>s, and they can't contain nested lists
#     or pre blocks. You can work around this by using HTML directly.
# - It takes some liberties with how inline (particularly block-level) HTML is processed and not
#     all HTML tags supported. HTML forms and <script/> tags are out.
# - Paragraphs in lists differ a bit from other markdowns. Use indented blank lines to get
#    to insert <br> tags to emulate paragraphs. Blank lines stop the list by inserting the
#    </ul> or </ol> tags.
#
# References:
# - https://tools.ietf.org/html/rfc7764
# - http://daringfireball.net/projects/markdown/syntax
# - https://guides.github.com/features/mastering-markdown/
# - http://fletcher.github.io/MultiMarkdown-4/syntax
# - http://spec.commonmark.org
#
# (c) 2016 Werner Stoop
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.

BEGIN {

	# Configuration options
	if(Title=="") Title = "Documentation";
	if(Theme=="") Theme = 1;
    #TopLinks = 1;
	#classic_underscore = 1;
    if(MaxWidth=="") MaxWidth="1080px";

    Mode = "none"; ToC = ""; ToCLevel = 1;
    CSS = init_css(Theme);
    for(i = 0; i < 128; i++)
        _ord[sprintf("%c", i)] = i;
    srand();
}

Mode == "none" && /\/\*\*/    {
    Mode = "p";
    gsub(/\/\*/,"");
}

Mode != "none" && /\*\//      {
    gsub(/\*\/.*$/,"");
    if(match($0, /^[[:space:]]*\*/))
        Out = Out filter(substr($0, RSTART+RLENGTH));
    if(Mode == "ul" || Mode == "ol") {
        while(ListLevel > 1)
            Buf = Buf "\n</" Open[ListLevel--] ">";
        Out = Out tag(Mode, Buf "\n");
    } else {
	    Buf = trim(scrub(Buf));
	    if(Buf)
            Out = Out tag(Mode, Buf);
    }
    Mode = "none";
    Buf = "";
}
Mode != "none" {
    gsub(/\r/, "", $0);
    if(match($0,/[[:graph:]]/) && substr($0,RSTART,1)!="*")
        next;
    gsub(/^[[:space:]]*\*/, "", $0);
}

Mode != "none" && /^[[:space:]]*\[[_ [:alnum:]]+\]:/ {
    linkdesc = ""; lastlink = 0;
    match($0,/\[.*\]/);
    LinkRef = tolower(substr($0, RSTART+1, RLENGTH-2));
    st = substr($0, RSTART+RLENGTH+2);
    match(st, /[^[:space:]]+/);
    url = substr(st, RSTART, RLENGTH);
    st = substr(st, RSTART+RLENGTH+1);
    if(match(url, /^<.*>/))
        url = substr(url, RSTART+1, RLENGTH-2);
    if(match(st, /["'(]/)) {
        delim = substr(st, RSTART, 1);
        edelim = (delim == "(") ? ")" : delim;
        if(match(st, delim ".*" edelim))
            linkdesc = substr(st, RSTART+1, RLENGTH-2);
    }
    LinkUrls[LinkRef] = escape(url);
    if(!linkdesc) lastlink = 1;
    LinkDescs[LinkRef] = escape(linkdesc);
    next;
}

Mode != "none" && lastlink && /^[[:space:]]*["'(]/ {
    match($0, /["'(]/);
    delim = substr($0, RSTART, 1);
    edelim = (delim == "(") ? ")" : delim;
    st = substr($0, RSTART);
    if(match(st, delim ".*" edelim))
        LinkDescs[LinkRef] = escape(substr(st,RSTART+1,RLENGTH-2));
    lastlink = 0;
    next;
}
lastlink { lastlink = 0; }
Mode == "p" && /^[[:space:]]*\[\^[_[:alnum:]]+\]:[[:space:]]*/ {
    match($0, /\[\^[[:alnum:]]+\]:/);
    name = substr($0, RSTART+2,RLENGTH-4);
    def = substr($0, RSTART+RLENGTH+1);
    Footnote[tolower(name)] = scrub(def);
    next;
}
Mode == "p" && /^[[:space:]]*\*\[[[:alnum:]]+\]:[[:space:]]*/ {
    match($0, /\[[[:alnum:]]+\]/);
    name = substr($0, RSTART+1,RLENGTH-2);
    def = substr($0, RSTART+RLENGTH+2);
    Abbrs[toupper(name)] = def;
    next;
}

Mode != "none" { Out = Out filter($0); }

END {
    print "<!DOCTYPE html>\n<html><head>"
    print "<title>" Title "</title>";
    if(stylesheet)
        print "<link rel=\"stylesheet\" href=\"" stylesheet "\">";
    else
        print "<style><!--" CSS "\n--></style>";
    print "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">";
	if(ToC && match(Out, /!\[toc[-+]?\]/))
		print "<script type=\"text/javascript\"><!--\n" \
			"function toggle_toc(n) {\n" \
			"   var toc=document.getElementById(\"table-of-contents-\" + n);\n" \
			"   var btn=document.getElementById(\"btn-text\");\n" \
			"   toc.style.display=(toc.style.display==\"none\")?\"block\":\"none\";\n" \
			"   btn.textContent=(toc.style.display==\"none\")?\"[+]\":\"[-]\";\n" \
			"}\n" \
			"//-->\n</script>";
    print "</head><body>";
    if(Out) {
        Out = fix_footnotes(Out);
        Out = fix_links(Out);
        Out = fix_abbrs(Out);
        Out = make_toc(Out);

        print trim(Out);
        if(footnotes) {
            footnotes = fix_links(footnotes);
            print "<hr><ol class=\"footnotes\">\n" footnotes "</ol>";
        }
    }
    print "</body></html>"
}

function escape(st) {
    gsub(/&/, "\\&", st);
    gsub(/</, "\\<", st);
    gsub(/>/, "\\>", st);
    return st;
}
function strip_tags(st) {
    gsub(/<\/?[^>]+>/,"",st);
    return st;
}
function trim(st) {
    sub(/^[[:space:]]+/, "", st);
    sub(/[[:space:]]+$/, "", st);
    return st;
}
function filter(st,       res,tmp) {
    if(Mode == "p") {
        if(match(st, /^((    )| *\t)/) || match(st, /^[[:space:]]*```/)) {
            preterm = trim(substr(st, RSTART,RLENGTH));
            st = substr(st, RSTART+RLENGTH);
            if(Buf) res = tag("p", scrub(Buf));
            Buf = st;
            push("pre");
        } else if(!trim(prev) && match(st, /^[[:space:]]*[*-][[:space:]]*[*-][[:space:]]*[*-][-*[:space:]]*$/)) {
            if(Buf) res = tag("p", scrub(Buf));
            Buf = "";
            res = res "
\n";
        } else if(match(st, /^[[:space:]]*===+[[:space:]]*$/)) {
            Buf = trim(substr(Buf, 1, length(Buf) - length(prev) - 1));
            if(Buf) res= tag("p", scrub(Buf));
            if(prev) res = res heading(1, scrub(prev));
            Buf = "";
        } else if(match(st, /^[[:space:]]*---+[[:space:]]*$/)) {
            Buf = trim(substr(Buf, 1, length(Buf) - length(prev) - 1));
            if(Buf) res = tag("p", scrub(Buf));
            if(prev) res = res heading(2, scrub(prev));
            Buf = "";
        } else if(match(st, /^[[:space:]]*#+/)) {
            sub(/#+[[:space:]]*$/, "", st);
            match(st, /#+/);
            ListLevel = RLENGTH;
            tmp = substr(st, RSTART+RLENGTH);
            if(Buf) res = tag("p", scrub(Buf));
            res = res heading(ListLevel, scrub(trim(tmp)));
            Buf = "";
        } else if(match(st, /^[[:space:]]*>/)) {
            if(Buf) res = tag("p", scrub(Buf));
            Buf = scrub(trim(substr(st, RSTART+RLENGTH)));
            push("blockquote");
        } else if(match(st, /^[[:space:]]*([*+-]|[[:digit:]]+\.)[[:space:]]/)) {
            if(Buf) res = tag("p", scrub(Buf));
            Buf="";
            match(st, /^[[:space:]]*/);
            ListLevel = 1;
            indent[ListLevel] = RLENGTH;
            Open[ListLevel]=match(st, /^[[:space:]]*[*+-][[:space:]]*/)?"ul":"ol";
            push(Open[ListLevel]);
            res = res filter(st);
        } else if(match(st, /^[[:space:]]*$/)) {
            if(trim(Buf)) {
                res = tag("p", scrub(trim(Buf)));
                Buf = "";
            }
        } else
            Buf = Buf st "\n";
    } else if(Mode == "blockquote") {
        if(match(st, /^[[:space:]]*>[[:space:]]*$/))
            Buf = Buf "\n

";
        else if(match(st, /^[[:space:]]*>/))
            Buf = Buf "\n" scrub(trim(substr(st, RSTART+RLENGTH)));
        else if(match(st, /^[[:space:]]*$/)) {
            res = tag("blockquote", tag("p", trim(Buf)));
            pop();
            res = res filter(st);
        } else
            Buf = Buf st;
    } else if(Mode == "pre") {
        if(!preterm && match(st, /^((    )| *\t)/) || preterm && !match(st, /^[[:space:]]*```/))
            Buf = Buf ((Buf)?"\n":"") substr(st, RSTART+RLENGTH);
        else {
            gsub(/\t/,"    ",Buf);
			if(length(trim(Buf)) > 0)
            	res = tag("pre", tag("code", escape(Buf)));
            pop();
            if(preterm) sub(/^[[:space:]]*```/,"",st);
            res = res filter(st);
        }
    } else if(Mode == "ul" || Mode == "ol") {
        if(ListLevel == 0 || match(st, /^[[:space:]]*$/) && (RLENGTH <= indent[1])) {
            while(ListLevel > 1)
                Buf = Buf "\n</" Open[ListLevel--] ">";
            res = tag(Mode, "\n" Buf "\n");
            pop();
        } else {
            if(match(st, /^[[:space:]]*([*+-]|[[:digit:]]+\.)/)) {
                tmp = substr(st, RLENGTH+1);
                match(st, /^[[:space:]]*/);
                if(RLENGTH > indent[ListLevel]) {
                    indent[++ListLevel] = RLENGTH;
                    if(match(st, /^[[:space:]]*[*+-]/))
                        Open[ListLevel] = "ul";
                    else
                        Open[ListLevel] = "ol";
                    Buf = Buf "\n<" Open[ListLevel] ">";
                } else while(RLENGTH < indent[ListLevel])
                    Buf = Buf "\n</" Open[ListLevel--] ">";
                if(match(tmp,/^[[:space:]]*\[[xX[:space:]]?\]/)) {
                    st = substr(tmp,RLENGTH+1);
                    tmp = tolower(substr(tmp,RSTART,RLENGTH));
                    Buf = Buf "
<input type=\"checkbox\" " (index(tmp,"x")?"checked":"") " disabled>" scrub(st);
                } else
                    Buf = Buf "
" scrub(tmp);
            } else if(match(st, /^[[:space:]]*$/)){
                Buf = Buf "
\n";
            } else {
                sub(/^[[:space:]]+/,"",st);
                Buf = Buf "\n" scrub(st);
            }
        }
    }
    prev = st;
    return res;
}
function scrub(st,    mp, ms, me, r, p, tg, a) {
	sub(/  $/,"
\n",st);
	gsub(/(  |[[:space:]]+\\)\n/,"
\n",st);
	while(match(st, /(__?|\*\*?|~~|`+|[&><\\])/)) {
		a = substr(st, 1, RSTART-1);
		mp = substr(st, RSTART, RLENGTH);
		ms = substr(st, RSTART-1,1);
		me = substr(st, RSTART+RLENGTH, 1);
		p = RSTART+RLENGTH;

		if(!classic_underscore && match(mp,/_+/)) {
			if(match(ms,/[[:alnum:]]/) && match(me,/[[:alnum:]]/)) {
				tg = substr(st, 1, index(st, mp));
				r = r tg;
				st = substr(st, index(st, mp) + 1);
				continue;
			}
		}
		st = substr(st, p);
		r = r a;
		ms = "";

		if(mp == "\\") {
			if(match(st, /^!?\[/)) {
				r = r "\\" substr(st, RSTART, RLENGTH);
				st = substr(st, 2);
			} else if(match(st, /^(\*\*|__|~~|`+)/)) {
				r = r substr(st, 1, RLENGTH);
				st = substr(st, RLENGTH+1);
			} else {
				r = r substr(st, 1, 1);
				st = substr(st, 2);
			}
			continue;
		} else if(mp == "_" || mp == "*") {
			if(match(me,/[[:space:]]/)) {
				r = r mp;
				continue;
			}
			p = index(st, mp);
			while(p && match(substr(st, p-1, 1),/[\\[:space:]]/)) {
				ms = ms substr(st, 1, p-1) mp;
				st = substr(st, p + length(mp));
				p = index(st, mp);
			}
			if(!p) {
				r = r mp ms;
				continue;
			}
			ms = ms substr(st,1,p-1);
			r = r itag("em", scrub(ms));
			st = substr(st,p+length(mp));
		} else if(mp == "__" || mp == "**") {
			if(match(me,/[[:space:]]/)) {
				r = r mp;
				continue;
			}
			p = index(st, mp);
			while(p && match(substr(st, p-1, 1),/[\\[:space:]]/)) {
				ms = ms substr(st, 1, p-1) mp;
				st = substr(st, p + length(mp));
				p = index(st, mp);
			}
			if(!p) {
				r = r mp ms;
				continue;
			}
			ms = ms substr(st,1,p-1);
			r = r itag("strong", scrub(ms));
			st = substr(st,p+length(mp));
		} else if(mp == "~~") {
			p = index(st, mp);
			if(!p) {
				r = r mp;
				continue;
			}
			while(p && substr(st, p-1, 1) == "\\") {
				ms = ms substr(st, 1, p-1) mp;
				st = substr(st, p + length(mp));
				p = index(st, mp);
			}
			ms = ms substr(st,1,p-1);
			r = r itag("del", scrub(ms));
			st = substr(st,p+length(mp));
		} else if(match(mp, /`+/)) {
			p = index(st, mp);
			if(!p) {
				r = r mp;
				continue;
			}
			ms = substr(st,1,p-1);
			r = r itag("code", escape(ms));
			st = substr(st,p+length(mp));
		} else if(mp == ">") {
			r = r ">";
		} else if(mp == "<") {

			p = index(st, ">");
			if(!p) {
				r = r "<";
				continue;
			}
			tg = substr(st, 1, p - 1);
			if(match(tg,/^[[:alpha:]]+[[:space:]]/)) {
				a = substr(tg,RSTART+RLENGTH-1);
				tg = substr(tg,1,RLENGTH-1);
			} else
				a = "";

			if(match(tolower(tg), "^/?(a|abbr|div|span|blockquote|pre|img|code|p|em|strong|sup|sub|del|ins|s|u|b|i|br|hr|ul|ol|li|table|thead|tfoot|tbody|tr|th|td|caption|column|col|colgroup|figure|figcaption|dl|dd|dt|mark|cite|q|var|samp|small)$")) {
				r = r "<" tg a ">";
			} else if(match(tg, "^[[:alpha:]]+://[[:graph:]]+$")) {
				if(!a) a = tg;
				r = r "" a "";
			} else if(match(tg, "^[[:graph:]]+@[[:graph:]]+$")) {
				if(!a) a = tg;
				r = r "" obfuscate(a) "";
			} else {
				r = r "<";
				continue;
			}

			st = substr(st, p + 1);
		} else if(mp == "&") {
			if(match(st, /^[#[:alnum:]]+;/)) {
				r = r "&" substr(st, 1, RLENGTH);
				st = substr(st, RLENGTH+1);
			} else {
				r = r "&";
			}
		}
	}
    return r st;
}

function push(newmode) {Stack[StackTop++] = Mode; Mode = newmode;}
function pop() {Mode = Stack[--StackTop];Buf = ""; return Mode;}
function heading(level, st,       res, href) {
    st = trim(st);
    if(level > 6) level = 6;
    href = tolower(st);
    href = strip_tags(href);
    gsub(/[^ [:alnum:]]+/, "", href);
    gsub(/ +/, "-", href);
    LinkUrls[href] = "#" href;
    LinkUrls[tolower(st)] = "#" href;
    res = tag("h" level, st (TopLinks?"  ↑ Top":""), "id=\"" href "\"");
    for(;ToCLevel < level; ToCLevel++)
        ToC = ToC "
";
    for(;ToCLevel > level; ToCLevel--)
        ToC = ToC "
";
    ToC = ToC "
" st "\n";
    ToCLevel = level;
    return res;
}
function make_toc(st,              r,p,dis,t,n) {
    if(!ToC) return st;
    for(;ToCLevel > 1;ToCLevel--)
        ToC = ToC "
";
    p = match(st, /!\[toc[-+]?\]/);
    while(p) {
		++n;
		dis = index(substr(st,RSTART,RLENGTH),"+");
		t = "
\n" (dis?"[-]":"[+]") " Contents\n" \
			"
\n
" ToC "
\n
\n
";
        r = r substr(st,1,RSTART-1);
        if(substr(st,RSTART-1,1) != "\\")
            r = r t;
        else
            r = substr(r,1,length(r)-1) substr(st,RSTART,RLENGTH);
        st = substr(st,RSTART+RLENGTH);
        p = match(st, /!\[toc[-+]?\]/);
    }
    return r st;
}
function fix_links(st,          lt,ld,lr,url,img,res,rx,pos) {
    do {
        pos = match(st, /\[[^\]]+\]/);
        if(!pos)break;
        img=substr(st,RSTART-1,1)=="!";
        if(substr(st, RSTART-(img?2:1),1)=="\\") {
            res = res substr(st,1,RSTART-(img?3:2));
            if(img && substr(st,RSTART,RLENGTH)=="[toc]")res=res "\\";
            res = res substr(st,RSTART-(img?1:0),RLENGTH+(img?1:0));
            st = substr(st, RSTART + RLENGTH);
            continue;
        }
        res = res substr(st, 1, RSTART-(img?2:1));
        rx = substr(st, RSTART, RLENGTH);
        st = substr(st, RSTART+RLENGTH);
        if(match(st, /^[[:space:]]*\([^)]+\)/)) {
            lt = substr(rx, 2, length(rx) - 2);
            match(st, /\([^)]+\)/);
            url = substr(st, RSTART+1, RLENGTH-2);
            st = substr(st, RSTART+RLENGTH);
            ld = "";
            if(match(url,/[[:space:]]+["']/)) {
                ld = url;
                url = substr(url, 1, RSTART - 1);
                match(ld,/["']/);
                delim = substr(ld, RSTART, 1);
                if(match(ld,delim ".*" delim))
                    ld = substr(ld, RSTART+1, RLENGTH-2);
            }  else ld = "";
            if(img)
                res = res "";
            else
                res = res "" lt "";
        } else if(match(st, /^[[:space:]]*\[[^\]]*\]/)) {
            lt = substr(rx, 2, length(rx) - 2);
            match(st, /\[[^\]]*\]/);
            lr = trim(tolower(substr(st, RSTART+1, RLENGTH-2)));
            if(!lr) {
                lr = tolower(trim(lt));
                if(LinkDescs[lr]) lt = LinkDescs[lr];
            }
            st = substr(st, RSTART+RLENGTH);
            url = LinkUrls[lr];
            ld = LinkDescs[lr];
            if(img)
                res = res "";
            else
                res = res "" lt "";
        } else
            res = res (img?"!":"") rx;
    } while(pos > 0);
    return res st;
}
function fix_footnotes(st,         r,p,n,i,d,fn,fc) {
    p = match(st, /\[\^[^\]]+\]/);
    while(p) {
        r = r substr(st,1,RSTART-1);
        d = substr(st,RSTART+2,RLENGTH-3);
        n = tolower(d);
        st = substr(st,RSTART+RLENGTH);
        if(Footnote[tolower(n)]) {
            if(!fn[n]) fn[n] = ++fc;
            d = Footnote[n];
        } else {
            Footnote[n] = scrub(d);
            if(!fn[n]) fn[n] = ++fc;
        }
        footname[fc] = n;
        d = strip_tags(d);
        if(length(d) > 20) d = substr(d,1,20) "…";
        r = r "[" fn[n] "]";
        p = match(st, /\[\^[^\]]+\]/);
    }
    for(i=1;i<=fc;i++)
        footnotes = footnotes "
" Footnote[footname[i]] \
            "  ↶ Back
\n";
    return r st;
}
function fix_abbrs(str,         st,k,r,p) {
	for(k in Abbrs) {
		r = "";
		st = str;
		t = escape(Abbrs[toupper(k)]);
		gsub(/&/,"\\&", t);
		p = match(st,"[^[:alnum:]]" k "[^[:alnum:]]");
		while(p) {
			r = r substr(st, 1, RSTART);
			r = r "" k "";
			st = substr(st, RSTART+RLENGTH-1);
			p = match(st,"[^[:alnum:]]" k "[^[:alnum:]]");
		}
		str = r st;
	}
	return str;
}
function tag(t, body, attr) {
    if(attr)
        attr = " " trim(attr);
    # https://www.w3.org/TR/html5/grouping-content.html#the-p-element
    if(t == "p" && (match(body, /<\/?(div|table|blockquote|dl|ol|ul|h[[:digit:]]|hr|pre)[>[:space:]]/))|| (match(body,/!\[toc\]/) && substr(body, RSTART-1,1) != "\\"))
        return "<" t attr ">" body "\n";
    else
        return "<" t attr ">" body "</" t ">\n";
}
function itag(t, body) {
    return "<" t ">" body "</" t ">";
}
function obfuscate(e,     r,i,t,o) {
    for(i = 1; i <= length(e); i++) {
        t = substr(e,i,1);
        r = int(rand() * 100);
        if(r > 50)
            o = o sprintf("&#x%02X;", _ord[t]);
        else if(r > 10)
            o = o sprintf("&#%d;", _ord[t]);
        else
            o = o t;
    }
    return o;
}
function bright(c,a ,r,g,b) {
    sub(/^#/,"",c);
    r = 255*a + _hex[toupper(substr(c,1,2))]*(1-a);
    g = 255*a + _hex[toupper(substr(c,3,2))]*(1-a);
    b = 255*a + _hex[toupper(substr(c,5,2))]*(1-a);
    return sprintf("#%02X%02X%02X",r>255?255:r,g>255?255:g,b>255?255:b);
}
function init_css(Theme,             css,ss,hr,c1,c2,c3,c4,c5,bg1,bg2,bg3,bg4,ff,i) {
    if(Theme == "0") return "";

    css["body"] = "color:%color1%;font-family:%font-family%;line-height:1.5em;" \
                "padding:1em 2em;width:80%;max-width:%maxwidth%;margin:0 auto;min-height:100%;float:none;";
    css["h1"] = "color:%color1%;border-bottom:1px solid %background1%;padding:0.3em 0.1em;";
    css["h2"] = "color:%color2%;border-bottom:1px solid %background2%;padding:0.2em 0.1em;";
    css["h3"] = "color:%color3%;border-bottom:1px solid %background3%;padding:0.1em 0.1em;";
    css["h4,h5,h6"] = "color:%color4%;padding:0.1em 0.1em;";
    css["h1,h2,h3,h4,h5,h6"] = "font-weight:normal;line-height:1.2em;";
	css["h4"] = "border-bottom:1px solid %background4%";
    css["p"] = "margin:0.5em 0.1em;"
    css["hr"] = "background:%hr%;height:1px;border:0;"
    css["a"] = "color:%color2%;";
    css["a:visited"] = "color:%color2%;";
    css["a:active"] = "color:%color4%;";
    css["a:hover"] = "color:%color4%;";
    css["a.top"] = "font-size:x-small;text-decoration:initial;float:right;";
    css["strong,b"] = "color:%color1%";
    css["code"] = "color:%color2%;";
    css["blockquote"] = "margin-left:1em;color:%color2%;background:%color5%;border-left:0.2em solid %color3%;border-radius:3px;padding:0.25em 0.5em;";
    css["pre"] = "color:%color2%;background:%color5%;border-radius:5px;line-height:1.25em;margin:0.25em 0.5em;padding:0.75em;";
    css["table"] = "border-collapse:collapse;margin:0.5em;";
    css["th,td"] = "padding:0.5em 0.75em;border:1px solid %color4%;";
    css["th"] = "color:%color2%;border:1px solid %color3%;border-bottom:2px solid %color3%;";
    css["tr:nth-child(odd)"] = "background-color:%color5%;";
    css["div"] = "padding:0.5em;";
    css["caption"] = "padding:0.5em;font-style:italic;";
    css["dl"] = "margin:0.5em;";
    css["dt"] = "font-weight:bold;";
    css["dd"] = "padding:0.3em;";
    css["mark"] = "color:%color2%;background-color:%color5%;";
    css["del,s"] = "color:%color4%;";
    css["a#toc-button"] = "color:%color3%;background:%color5%;cursor:pointer;font-size:small;padding: 0.3em 0.5em 0.5em 0.5em;font-family:monospace;border-radius:3px;";
    css["a#toc-button:hover"] = "color:%color5%;background:%color3%;";
    css["div#table-of-contents"] = "padding:0;font-size:smaller;";
    css["abbr"] = "cursor:help;";
    css["ol.footnotes"] = "font-size:small;color:%color4%";
    css["a.footnote"] = "font-size:smaller;text-decoration:initial;";
    css["a.footnote-back"] = "text-decoration:initial;font-size:x-small;";
    css[".fade"] = "color:%color5%;";
    css[".highlight"] = "color:%color2%;background-color:%color5%;";

    if(Theme==2){
        c1="#303F9F";c2="#0449CC";c3="#2162FA";c4="#4B80FB";c5="#EDF2FF";
        ff="\"Trebuchet MS\", Helvetica, sans-serif";
    } else if(Theme==3){
        c1="#430005";c2="#740009";c3="#A6373F";c4="#c55158";c5="#fbf2f2";
        ff="Verdana, Geneva, sans-serif";
    } else if(Theme==4){
        c1="#083900";c2="#0D6300";c3="#3C8D2F";c4="#50be3f";c5="#f2faf1";
        ff="Georgia, serif";
    } else if(Theme==5){
        c1="#453700";c2="#775F00";c3="#AA9339";c4="#c7b057";c5="#fbf9f2";
        ff="Tahoma, Geneva, sans-serif";
    } else if(Theme==6){
        c1="#315067";c2="#4F9E9C";c3="#77AD93";c4="#95CE94";c5="#F6FFEE";
        ff="Verdana, sans-serif;";
    } else if(Theme==7){
        c1="#35305D";c2="#646379";c3="#7A74A5";c4="#646392";c5="#fafafa";
    } else if(Theme==8){
        c1="#215FC2";c2="#D49095";c3="#AD90D4";c4="#90D4CF";c5="#F7FDEF";
    } else {
        c1="#092859";c2="#1351b5";c3="#d47034";c4="#DC7435";c5="#F6F8FF";
    }
	if(!ff) ff = "sans-serif"

    for(i = 0; i<=255; i++)_hex[sprintf("%02X",i)]=i;
    bg1 = bright(c1,0.5);
    bg2 = bright(c2,0.5);
    bg3 = bright(c3,0.5);
    bg4 = bright(c3,0.75);
    hr = bright(c1,0.75);
    for(k in css)
        ss = ss "\n" k "{" css[k] "}";
    gsub(/%maxwidth%/,MaxWidth,ss);
    gsub(/%color1%/,c1,ss);
    gsub(/%color2%/,c2,ss);
    gsub(/%color3%/,c3,ss);
    gsub(/%color4%/,c4,ss);
    gsub(/%color5%/,c5,ss);
    gsub(/%background1%/,bg1,ss);
    gsub(/%background2%/,bg2,ss);
    gsub(/%background3%/,bg3,ss);
    gsub(/%background4%/,bg4,ss);
    gsub(/%font-family%/,ff,ss);
    gsub(/%hr%/,hr,ss);
    return ss;
}

#! /usr/bin/awk -f
#
# Purpose: Either converts markdown in code comments
# to HTML, or is a poem about Hastur. I forget.
#
# The comments must have the /** */ pattern. Every line in the comment
# must start with a *. Like so:
#
# /**
#  * Markdown here...
#  */
#
# Configuration options. You can set these in the BEGIN block below, or
# pass them to the script through the `-v` command-line option:
# - `Title="My Document Title"` to set the <title/> in the  section of the HTML
# - `stylesheet = "style.css"` to use a separate file as style sheet.
# - `Theme=n` with n=[1-8] to use one of the predefined color themes.
#        Default = Theme 1. Theme 0 disables all stylesheets.
# - `TopLinks=1` to have links to the top of the doc next to headers.
# - `classic_underscore=1` words_with_underscores behave like old markdown where the
#        underscores in the word counts as emphasis. The default behaviour is to have
#        `words_like_this` not contain any emphasis.
#
# I've tested it with Gawk, Nawk and Mawk.
# Gawk and Nawk worked without issues, but don't use the -W compat
# or -W traditional settings with Gawk.
# Mawk v1.3.4 worked correctly but v1.3.3 choked on it.
#
# Extensions:
# - A link like [link text][heading-name] gets replaced with link text
#   where heading-name corresponds to one of the headings.
# - Insert a Table of Contents by using ![toc]. The Table of Contents is collapsed by default,
#   use ![toc+] to insert a ToC that is expanded by default; ![toc-] for a collapsed ToC.
# - Github-style ``` code blocks supported.
# - Github-style ~~strikethrough~~ supported.
# - GitHub-style task lists `- [x]` are supported for documenting bugs and todo lists in code.
# - A couple of ideas from MultiMarkdown:
#    - [^footnotes] are supported.
#    - *[abbr]: Abbreviations are supported.
#    - Space followed by \ before a newline also forces a line break.
# - Default behaviour is to have words_like_this not contain emphasis.
# Limitations:
# - You can't nest <blockquote>s, and they can't contain nested lists
#     or pre blocks. You can work around this by using HTML directly.
# - It takes some liberties with how inline (particularly block-level) HTML is processed and not
#     all HTML tags supported. HTML forms and <script/> tags are out.
# - Paragraphs in lists differ a bit from other markdowns. Use indented blank lines to get
#    to insert <br> tags to emulate paragraphs. Blank lines stop the list by inserting the
#    </ul> or </ol> tags.
#
# References:
# - https://tools.ietf.org/html/rfc7764
# - http://daringfireball.net/projects/markdown/syntax
# - https://guides.github.com/features/mastering-markdown/
# - http://fletcher.github.io/MultiMarkdown-4/syntax
# - http://spec.commonmark.org
#
# (c) 2016 Werner Stoop
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.

BEGIN {

	# Configuration options
	if(Title=="") Title = "Documentation";
	if(Theme=="") Theme = 1;
    #TopLinks = 1;
	#classic_underscore = 1;
    if(MaxWidth=="") MaxWidth="1080px";

    Mode = "none"; ToC = ""; ToCLevel = 1;
    CSS = init_css(Theme);
    for(i = 0; i < 128; i++)
        _ord[sprintf("%c", i)] = i;
    srand();
}

Mode == "none" && /\/\*\*/    {
    Mode = "p";
    gsub(/\/\*/,"");
}

Mode != "none" && /\*\//      {
    gsub(/\*\/.*$/,"");
    if(match($0, /^[[:space:]]*\*/))
        Out = Out filter(substr($0, RSTART+RLENGTH));
    if(Mode == "ul" || Mode == "ol") {
        while(ListLevel > 1)
            Buf = Buf "\n</" Open[ListLevel--] ">";
        Out = Out tag(Mode, Buf "\n");
    } else {
	    Buf = trim(scrub(Buf));
	    if(Buf)
            Out = Out tag(Mode, Buf);
    }
    Mode = "none";
    Buf = "";
}
Mode != "none" {
    gsub(/\r/, "", $0);
    if(match($0,/[[:graph:]]/) && substr($0,RSTART,1)!="*")
        next;
    gsub(/^[[:space:]]*\*/, "", $0);
}

Mode != "none" && /^[[:space:]]*\[[_ [:alnum:]]+\]:/ {
    linkdesc = ""; lastlink = 0;
    match($0,/\[.*\]/);
    LinkRef = tolower(substr($0, RSTART+1, RLENGTH-2));
    st = substr($0, RSTART+RLENGTH+2);
    match(st, /[^[:space:]]+/);
    url = substr(st, RSTART, RLENGTH);
    st = substr(st, RSTART+RLENGTH+1);
    if(match(url, /^<.*>/))
        url = substr(url, RSTART+1, RLENGTH-2);
    if(match(st, /["'(]/)) {
        delim = substr(st, RSTART, 1);
        edelim = (delim == "(") ? ")" : delim;
        if(match(st, delim ".*" edelim))
            linkdesc = substr(st, RSTART+1, RLENGTH-2);
    }
    LinkUrls[LinkRef] = escape(url);
    if(!linkdesc) lastlink = 1;
    LinkDescs[LinkRef] = escape(linkdesc);
    next;
}

Mode != "none" && lastlink && /^[[:space:]]*["'(]/ {
    match($0, /["'(]/);
    delim = substr($0, RSTART, 1);
    edelim = (delim == "(") ? ")" : delim;
    st = substr($0, RSTART);
    if(match(st, delim ".*" edelim))
        LinkDescs[LinkRef] = escape(substr(st,RSTART+1,RLENGTH-2));
    lastlink = 0;
    next;
}
lastlink { lastlink = 0; }
Mode == "p" && /^[[:space:]]*\[\^[_[:alnum:]]+\]:[[:space:]]*/ {
    match($0, /\[\^[[:alnum:]]+\]:/);
    name = substr($0, RSTART+2,RLENGTH-4);
    def = substr($0, RSTART+RLENGTH+1);
    Footnote[tolower(name)] = scrub(def);
    next;
}
Mode == "p" && /^[[:space:]]*\*\[[[:alnum:]]+\]:[[:space:]]*/ {
    match($0, /\[[[:alnum:]]+\]/);
    name = substr($0, RSTART+1,RLENGTH-2);
    def = substr($0, RSTART+RLENGTH+2);
    Abbrs[toupper(name)] = def;
    next;
}

Mode != "none" { Out = Out filter($0); }

END {
    print "<!DOCTYPE html>\n<html><head>"
    print "<title>" Title "</title>";
    if(stylesheet)
        print "<link rel=\"stylesheet\" href=\"" stylesheet "\">";
    else
        print "<style><!--" CSS "\n--></style>";
    print "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">";
	if(ToC && match(Out, /!\[toc[-+]?\]/))
		print "<script type=\"text/javascript\"><!--\n" \
			"function toggle_toc(n) {\n" \
			"   var toc=document.getElementById(\"table-of-contents-\" + n);\n" \
			"   var btn=document.getElementById(\"btn-text\");\n" \
			"   toc.style.display=(toc.style.display==\"none\")?\"block\":\"none\";\n" \
			"   btn.textContent=(toc.style.display==\"none\")?\"[+]\":\"[-]\";\n" \
			"}\n" \
			"//-->\n</script>";
    print "</head><body>";
    if(Out) {
        Out = fix_footnotes(Out);
        Out = fix_links(Out);
        Out = fix_abbrs(Out);
        Out = make_toc(Out);

        print trim(Out);
        if(footnotes) {
            footnotes = fix_links(footnotes);
            print "<hr><ol class=\"footnotes\">\n" footnotes "</ol>";
        }
    }
    print "</body></html>"
}

function escape(st) {
    gsub(/&/, "\\&", st);
    gsub(/</, "\\<", st);
    gsub(/>/, "\\>", st);
    return st;
}
function strip_tags(st) {
    gsub(/<\/?[^>]+>/,"",st);
    return st;
}
function trim(st) {
    sub(/^[[:space:]]+/, "", st);
    sub(/[[:space:]]+$/, "", st);
    return st;
}
function filter(st,       res,tmp) {
    if(Mode == "p") {
        if(match(st, /^((    )| *\t)/) || match(st, /^[[:space:]]*```/)) {
            preterm = trim(substr(st, RSTART,RLENGTH));
            st = substr(st, RSTART+RLENGTH);
            if(Buf) res = tag("p", scrub(Buf));
            Buf = st;
            push("pre");
        } else if(!trim(prev) && match(st, /^[[:space:]]*[*-][[:space:]]*[*-][[:space:]]*[*-][-*[:space:]]*$/)) {
            if(Buf) res = tag("p", scrub(Buf));
            Buf = "";
            res = res "
\n";
        } else if(match(st, /^[[:space:]]*===+[[:space:]]*$/)) {
            Buf = trim(substr(Buf, 1, length(Buf) - length(prev) - 1));
            if(Buf) res= tag("p", scrub(Buf));
            if(prev) res = res heading(1, scrub(prev));
            Buf = "";
        } else if(match(st, /^[[:space:]]*---+[[:space:]]*$/)) {
            Buf = trim(substr(Buf, 1, length(Buf) - length(prev) - 1));
            if(Buf) res = tag("p", scrub(Buf));
            if(prev) res = res heading(2, scrub(prev));
            Buf = "";
        } else if(match(st, /^[[:space:]]*#+/)) {
            sub(/#+[[:space:]]*$/, "", st);
            match(st, /#+/);
            ListLevel = RLENGTH;
            tmp = substr(st, RSTART+RLENGTH);
            if(Buf) res = tag("p", scrub(Buf));
            res = res heading(ListLevel, scrub(trim(tmp)));
            Buf = "";
        } else if(match(st, /^[[:space:]]*>/)) {
            if(Buf) res = tag("p", scrub(Buf));
            Buf = scrub(trim(substr(st, RSTART+RLENGTH)));
            push("blockquote");
        } else if(match(st, /^[[:space:]]*([*+-]|[[:digit:]]+\.)/)) {
            if(Buf) res = tag("p", scrub(Buf));
            Buf="";
            match(st, /^[[:space:]]*/);
            ListLevel = 1;
            indent[ListLevel] = RLENGTH;
            Open[ListLevel]=match(st, /^[[:space:]]*[*+-][[:space:]]*/)?"ul":"ol";
            push(Open[ListLevel]);
            res = res filter(st);
        } else if(match(st, /^[[:space:]]*$/)) {
            if(trim(Buf)) {
                res = tag("p", scrub(trim(Buf)));
                Buf = "";
            }
        } else
            Buf = Buf st "\n";
    } else if(Mode == "blockquote") {
        if(match(st, /^[[:space:]]*>[[:space:]]*$/))
            Buf = Buf "\n

";
        else if(match(st, /^[[:space:]]*>/))
            Buf = Buf "\n" scrub(trim(substr(st, RSTART+RLENGTH)));
        else if(match(st, /^[[:space:]]*$/)) {
            res = tag("blockquote", tag("p", trim(Buf)));
            pop();
            res = res filter(st);
        } else
            Buf = Buf st;
    } else if(Mode == "pre") {
        if(!preterm && match(st, /^((    )| *\t)/) || preterm && !match(st, /^[[:space:]]*```/))
            Buf = Buf ((Buf)?"\n":"") substr(st, RSTART+RLENGTH);
        else {
            gsub(/\t/,"    ",Buf);
			if(length(trim(Buf)) > 0)
            	res = tag("pre", tag("code", escape(Buf)));
            pop();
            if(preterm) sub(/^[[:space:]]*```/,"",st);
            res = res filter(st);
        }
    } else if(Mode == "ul" || Mode == "ol") {
        if(ListLevel == 0 || match(st, /^[[:space:]]*$/) && (RLENGTH <= indent[1])) {
            while(ListLevel > 1)
                Buf = Buf "\n</" Open[ListLevel--] ">";
            res = tag(Mode, "\n" Buf "\n");
            pop();
        } else {
            if(match(st, /^[[:space:]]*([*+-]|[[:digit:]]+\.)/)) {
                tmp = substr(st, RLENGTH+1);
                match(st, /^[[:space:]]*/);
                if(RLENGTH > indent[ListLevel]) {
                    indent[++ListLevel] = RLENGTH;
                    if(match(st, /^[[:space:]]*[*+-]/))
                        Open[ListLevel] = "ul";
                    else
                        Open[ListLevel] = "ol";
                    Buf = Buf "\n<" Open[ListLevel] ">";
                } else while(RLENGTH < indent[ListLevel])
                    Buf = Buf "\n</" Open[ListLevel--] ">";
                if(match(tmp,/^[[:space:]]*\[[xX[:space:]]?\]/)) {
                    st = substr(tmp,RLENGTH+1);
                    tmp = tolower(substr(tmp,RSTART,RLENGTH));
                    Buf = Buf "
<input type=\"checkbox\" " (index(tmp,"x")?"checked":"") " disabled>" scrub(st);
                } else
                    Buf = Buf "
" scrub(tmp);
            } else if(match(st, /^[[:space:]]*$/)){
                Buf = Buf "
\n";
            } else {
                sub(/^[[:space:]]+/,"",st);
                Buf = Buf "\n" scrub(st);
            }
        }
    }
    prev = st;
    return res;
}
function scrub(st,    mp, ms, me, r, p, tg, a) {
	sub(/  $/,"
\n",st);
	gsub(/(  |[[:space:]]+\\)\n/,"
\n",st);
	while(match(st, /(__?|\*\*?|~~|`+|[&><\\])/)) {
		a = substr(st, 1, RSTART-1);
		mp = substr(st, RSTART, RLENGTH);
		ms = substr(st, RSTART-1,1);
		me = substr(st, RSTART+RLENGTH, 1);
		p = RSTART+RLENGTH;

		if(!classic_underscore && match(mp,/_+/)) {
			if(match(ms,/[[:alnum:]]/) && match(me,/[[:alnum:]]/)) {
				tg = substr(st, 1, index(st, mp));
				r = r tg;
				st = substr(st, index(st, mp) + 1);
				continue;
			}
		}
		st = substr(st, p);
		r = r a;
		ms = "";

		if(mp == "\\") {
			if(match(st, /^!?\[/)) {
				r = r "\\" substr(st, RSTART, RLENGTH);
				st = substr(st, 2);
			} else if(match(st, /^(\*\*|__|~~|`+)/)) {
				r = r substr(st, 1, RLENGTH);
				st = substr(st, RLENGTH+1);
			} else {
				r = r substr(st, 1, 1);
				st = substr(st, 2);
			}
			continue;
		} else if(mp == "_" || mp == "*") {
			if(match(me,/[[:space:]]/)) {
				r = r mp;
				continue;
			}
			p = index(st, mp);
			while(p && match(substr(st, p-1, 1),/[\\[:space:]]/)) {
				ms = ms substr(st, 1, p-1) mp;
				st = substr(st, p + length(mp));
				p = index(st, mp);
			}
			if(!p) {
				r = r mp ms;
				continue;
			}
			ms = ms substr(st,1,p-1);
			r = r itag("em", scrub(ms));
			st = substr(st,p+length(mp));
		} else if(mp == "__" || mp == "**") {
			if(match(me,/[[:space:]]/)) {
				r = r mp;
				continue;
			}
			p = index(st, mp);
			while(p && match(substr(st, p-1, 1),/[\\[:space:]]/)) {
				ms = ms substr(st, 1, p-1) mp;
				st = substr(st, p + length(mp));
				p = index(st, mp);
			}
			if(!p) {
				r = r mp ms;
				continue;
			}
			ms = ms substr(st,1,p-1);
			r = r itag("strong", scrub(ms));
			st = substr(st,p+length(mp));
		} else if(mp == "~~") {
			p = index(st, mp);
			if(!p) {
				r = r mp;
				continue;
			}
			while(p && substr(st, p-1, 1) == "\\") {
				ms = ms substr(st, 1, p-1) mp;
				st = substr(st, p + length(mp));
				p = index(st, mp);
			}
			ms = ms substr(st,1,p-1);
			r = r itag("del", scrub(ms));
			st = substr(st,p+length(mp));
		} else if(match(mp, /`+/)) {
			p = index(st, mp);
			if(!p) {
				r = r mp;
				continue;
			}
			ms = substr(st,1,p-1);
			r = r itag("code", escape(ms));
			st = substr(st,p+length(mp));
		} else if(mp == ">") {
			r = r ">";
		} else if(mp == "<") {

			p = index(st, ">");
			if(!p) {
				r = r "<";
				continue;
			}
			tg = substr(st, 1, p - 1);
			if(match(tg,/^[[:alpha:]]+[[:space:]]/)) {
				a = substr(tg,RSTART+RLENGTH-1);
				tg = substr(tg,1,RLENGTH-1);
			} else
				a = "";

			if(match(tolower(tg), "^/?(a|abbr|div|span|blockquote|pre|img|code|p|em|strong|sup|sub|del|ins|s|u|b|i|br|hr|ul|ol|li|table|thead|tfoot|tbody|tr|th|td|caption|column|col|colgroup|figure|figcaption|dl|dd|dt|mark|cite|q|var|samp|small)$")) {
				r = r "<" tg a ">";
			} else if(match(tg, "^[[:alpha:]]+://[[:graph:]]+$")) {
				if(!a) a = tg;
				r = r "" a "";
			} else if(match(tg, "^[[:graph:]]+@[[:graph:]]+$")) {
				if(!a) a = tg;
				r = r "" obfuscate(a) "";
			} else {
				r = r "<";
				continue;
			}

			st = substr(st, p + 1);
		} else if(mp == "&") {
			if(match(st, /^[#[:alnum:]]+;/)) {
				r = r "&" substr(st, 1, RLENGTH);
				st = substr(st, RLENGTH+1);
			} else {
				r = r "&";
			}
		}
	}
    return r st;
}

function push(newmode) {Stack[StackTop++] = Mode; Mode = newmode;}
function pop() {Mode = Stack[--StackTop];Buf = ""; return Mode;}
function heading(level, st,       res, href) {
    st = trim(st);
    if(level > 6) level = 6;
    href = tolower(st);
    href = strip_tags(href);
    gsub(/[^ [:alnum:]]+/, "", href);
    gsub(/ +/, "-", href);
    LinkUrls[href] = "#" href;
    LinkUrls[tolower(st)] = "#" href;
    res = tag("h" level, st (TopLinks?"  ↑ Top":""), "id=\"" href "\"");
    for(;ToCLevel < level; ToCLevel++)
        ToC = ToC "
";
    for(;ToCLevel > level; ToCLevel--)
        ToC = ToC "
";
    ToC = ToC "
" st "\n";
    ToCLevel = level;
    return res;
}
function make_toc(st,              r,p,dis,t,n) {
    if(!ToC) return st;
    for(;ToCLevel > 1;ToCLevel--)
        ToC = ToC "
";
    p = match(st, /!\[toc[-+]?\]/);
    while(p) {
		++n;
		dis = index(substr(st,RSTART,RLENGTH),"+");
		t = "
\n" (dis?"[-]":"[+]") " Contents\n" \
			"
\n
" ToC "
\n
\n
";
        r = r substr(st,1,RSTART-1);
        if(substr(st,RSTART-1,1) != "\\")
            r = r t;
        else
            r = substr(r,1,length(r)-1) substr(st,RSTART,RLENGTH);
        st = substr(st,RSTART+RLENGTH);
        p = match(st, /!\[toc[-+]?\]/);
    }
    return r st;
}
function fix_links(st,          lt,ld,lr,url,img,res,rx,pos) {
    do {
        pos = match(st, /\[[^\]]+\]/);
        if(!pos)break;
        img=substr(st,RSTART-1,1)=="!";
        if(substr(st, RSTART-(img?2:1),1)=="\\") {
            res = res substr(st,1,RSTART-(img?3:2));
            if(img && substr(st,RSTART,RLENGTH)=="[toc]")res=res "\\";
            res = res substr(st,RSTART-(img?1:0),RLENGTH+(img?1:0));
            st = substr(st, RSTART + RLENGTH);
            continue;
        }
        res = res substr(st, 1, RSTART-(img?2:1));
        rx = substr(st, RSTART, RLENGTH);
        st = substr(st, RSTART+RLENGTH);
        if(match(st, /^[[:space:]]*\([^)]+\)/)) {
            lt = substr(rx, 2, length(rx) - 2);
            match(st, /\([^)]+\)/);
            url = substr(st, RSTART+1, RLENGTH-2);
            st = substr(st, RSTART+RLENGTH);
            ld = "";
            if(match(url,/[[:space:]]+["']/)) {
                ld = url;
                url = substr(url, 1, RSTART - 1);
                match(ld,/["']/);
                delim = substr(ld, RSTART, 1);
                if(match(ld,delim ".*" delim))
                    ld = substr(ld, RSTART+1, RLENGTH-2);
            }  else ld = "";
            if(img)
                res = res "";
            else
                res = res "" lt "";
        } else if(match(st, /^[[:space:]]*\[[^\]]*\]/)) {
            lt = substr(rx, 2, length(rx) - 2);
            match(st, /\[[^\]]*\]/);
            lr = trim(tolower(substr(st, RSTART+1, RLENGTH-2)));
            if(!lr) {
                lr = tolower(trim(lt));
                if(LinkDescs[lr]) lt = LinkDescs[lr];
            }
            st = substr(st, RSTART+RLENGTH);
            url = LinkUrls[lr];
            ld = LinkDescs[lr];
            if(img)
                res = res "";
            else
                res = res "" lt "";
        } else
            res = res (img?"!":"") rx;
    } while(pos > 0);
    return res st;
}
function fix_footnotes(st,         r,p,n,i,d,fn,fc) {
    p = match(st, /\[\^[^\]]+\]/);
    while(p) {
        r = r substr(st,1,RSTART-1);
        d = substr(st,RSTART+2,RLENGTH-3);
        n = tolower(d);
        st = substr(st,RSTART+RLENGTH);
        if(Footnote[tolower(n)]) {
            if(!fn[n]) fn[n] = ++fc;
            d = Footnote[n];
        } else {
            Footnote[n] = scrub(d);
            if(!fn[n]) fn[n] = ++fc;
        }
        footname[fc] = n;
        d = strip_tags(d);
        if(length(d) > 20) d = substr(d,1,20) "…";
        r = r "[" fn[n] "]";
        p = match(st, /\[\^[^\]]+\]/);
    }
    for(i=1;i<=fc;i++)
        footnotes = footnotes "
" Footnote[footname[i]] \
            "  ↶ Back
\n";
    return r st;
}
function fix_abbrs(str,         st,k,r,p) {
	for(k in Abbrs) {
		r = "";
		st = str;
		t = escape(Abbrs[toupper(k)]);
		gsub(/&/,"\\&", t);
		p = match(st,"[^[:alnum:]]" k "[^[:alnum:]]");
		while(p) {
			r = r substr(st, 1, RSTART);
			r = r "" k "";
			st = substr(st, RSTART+RLENGTH-1);
			p = match(st,"[^[:alnum:]]" k "[^[:alnum:]]");
		}
		str = r st;
	}
	return str;
}
function tag(t, body, attr) {
    if(attr)
        attr = " " trim(attr);
    # https://www.w3.org/TR/html5/grouping-content.html#the-p-element
    if(t == "p" && (match(body, /<\/?(div|table|blockquote|dl|ol|ul|h[[:digit:]]|hr|pre)[>[:space:]]/))|| (match(body,/!\[toc\]/) && substr(body, RSTART-1,1) != "\\"))
        return "<" t attr ">" body "\n";
    else
        return "<" t attr ">" body "</" t ">\n";
}
function itag(t, body) {
    return "<" t ">" body "</" t ">";
}
function obfuscate(e,     r,i,t,o) {
    for(i = 1; i <= length(e); i++) {
        t = substr(e,i,1);
        r = int(rand() * 100);
        if(r > 50)
            o = o sprintf("&#x%02X;", _ord[t]);
        else if(r > 10)
            o = o sprintf("&#%d;", _ord[t]);
        else
            o = o t;
    }
    return o;
}
function bright(c,a ,r,g,b) {
    sub(/^#/,"",c);
    r = 255*a + _hex[toupper(substr(c,1,2))]*(1-a);
    g = 255*a + _hex[toupper(substr(c,3,2))]*(1-a);
    b = 255*a + _hex[toupper(substr(c,5,2))]*(1-a);
    return sprintf("#%02X%02X%02X",r>255?255:r,g>255?255:g,b>255?255:b);
}
function init_css(Theme,             css,ss,hr,c1,c2,c3,c4,c5,bg1,bg2,bg3,bg4,ff,i) {
    if(Theme == "0") return "";

    css["body"] = "color:%color1%;font-family:%font-family%;line-height:1.5em;" \
                "padding:1em 2em;width:80%;max-width:%maxwidth%;margin:0 auto;min-height:100%;float:none;";
    css["h1"] = "color:%color1%;border-bottom:1px solid %background1%;padding:0.3em 0.1em;";
    css["h2"] = "color:%color2%;border-bottom:1px solid %background2%;padding:0.2em 0.1em;";
    css["h3"] = "color:%color3%;border-bottom:1px solid %background3%;padding:0.1em 0.1em;";
    css["h4,h5,h6"] = "color:%color4%;padding:0.1em 0.1em;";
    css["h1,h2,h3,h4,h5,h6"] = "font-weight:normal;line-height:1.2em;";
	css["h4"] = "border-bottom:1px solid %background4%";
    css["p"] = "margin:0.5em 0.1em;"
    css["hr"] = "background:%hr%;height:1px;border:0;"
    css["a"] = "color:%color2%;";
    css["a:visited"] = "color:%color2%;";
    css["a:active"] = "color:%color4%;";
    css["a:hover"] = "color:%color4%;";
    css["a.top"] = "font-size:x-small;text-decoration:initial;float:right;";
    css["strong,b"] = "color:%color1%";
    css["code"] = "color:%color2%;";
    css["blockquote"] = "margin-left:1em;color:%color2%;background:%color5%;border-left:0.2em solid %color3%;border-radius:3px;padding:0.25em 0.5em;";
    css["pre"] = "color:%color2%;background:%color5%;border-radius:5px;line-height:1.25em;margin:0.25em 0.5em;padding:0.75em;";
    css["table"] = "border-collapse:collapse;margin:0.5em;";
    css["th,td"] = "padding:0.5em 0.75em;border:1px solid %color4%;";
    css["th"] = "color:%color2%;border:1px solid %color3%;border-bottom:2px solid %color3%;";
    css["tr:nth-child(odd)"] = "background-color:%color5%;";
    css["div"] = "padding:0.5em;";
    css["caption"] = "padding:0.5em;font-style:italic;";
    css["dl"] = "margin:0.5em;";
    css["dt"] = "font-weight:bold;";
    css["dd"] = "padding:0.3em;";
    css["mark"] = "color:%color2%;background-color:%color5%;";
    css["del,s"] = "color:%color4%;";
    css["a#toc-button"] = "color:%color3%;background:%color5%;cursor:pointer;font-size:small;padding: 0.3em 0.5em 0.5em 0.5em;font-family:monospace;border-radius:3px;";
    css["a#toc-button:hover"] = "color:%color5%;background:%color3%;";
    css["div#table-of-contents"] = "padding:0;font-size:smaller;";
    css["abbr"] = "cursor:help;";
    css["ol.footnotes"] = "font-size:small;color:%color4%";
    css["a.footnote"] = "font-size:smaller;text-decoration:initial;";
    css["a.footnote-back"] = "text-decoration:initial;font-size:x-small;";
    css[".fade"] = "color:%color5%;";
    css[".highlight"] = "color:%color2%;background-color:%color5%;";

    if(Theme==2){
        c1="#303F9F";c2="#0449CC";c3="#2162FA";c4="#4B80FB";c5="#EDF2FF";
        ff="\"Trebuchet MS\", Helvetica, sans-serif";
    } else if(Theme==3){
        c1="#430005";c2="#740009";c3="#A6373F";c4="#c55158";c5="#fbf2f2";
        ff="Verdana, Geneva, sans-serif";
    } else if(Theme==4){
        c1="#083900";c2="#0D6300";c3="#3C8D2F";c4="#50be3f";c5="#f2faf1";
        ff="Georgia, serif";
    } else if(Theme==5){
        c1="#453700";c2="#775F00";c3="#AA9339";c4="#c7b057";c5="#fbf9f2";
        ff="Tahoma, Geneva, sans-serif";
    } else if(Theme==6){
        c1="#315067";c2="#4F9E9C";c3="#77AD93";c4="#95CE94";c5="#F6FFEE";
        ff="Verdana, sans-serif;";
    } else if(Theme==7){
        c1="#35305D";c2="#646379";c3="#7A74A5";c4="#646392";c5="#fafafa";
    } else if(Theme==8){
        c1="#215FC2";c2="#D49095";c3="#AD90D4";c4="#90D4CF";c5="#F7FDEF";
    } else {
        c1="#092859";c2="#1351b5";c3="#d47034";c4="#DC7435";c5="#F6F8FF";
    }
	if(!ff) ff = "sans-serif"

    for(i = 0; i<=255; i++)_hex[sprintf("%02X",i)]=i;
    bg1 = bright(c1,0.5);
    bg2 = bright(c2,0.5);
    bg3 = bright(c3,0.5);
    bg4 = bright(c3,0.75);
    hr = bright(c1,0.75);
    for(k in css)
        ss = ss "\n" k "{" css[k] "}";
    gsub(/%maxwidth%/,MaxWidth,ss);
    gsub(/%color1%/,c1,ss);
    gsub(/%color2%/,c2,ss);
    gsub(/%color3%/,c3,ss);
    gsub(/%color4%/,c4,ss);
    gsub(/%color5%/,c5,ss);
    gsub(/%background1%/,bg1,ss);
    gsub(/%background2%/,bg2,ss);
    gsub(/%background3%/,bg3,ss);
    gsub(/%background4%/,bg4,ss);
    gsub(/%font-family%/,ff,ss);
    gsub(/%hr%/,hr,ss);
    return ss;
}


FIxup jamaicainn & newworlds bonus seasonNumber

# Create column files with lists of series titles, descriptions, number
# of seasons, and number of episodes, to be pasted into a spreadsheet.
# Note that IN_CANADA affects processing.
#
# Create a separate file with a line for each episode containing
# seriesNumber, episodeURL, seriesTitle, seasonNumber, episodeNumber,
# episodeTitle, & episodeDuration with the same columns as the primary
# spreadsheet so they can be combined into one.
#
# Create a separate file with a line for each series duration
#
# Return the total duration of all series
#
# INVOCATION:
#       curl -s https://acorn.tv/192/ \
#           | awk -v TITLE_FILE=$TITLE_FILE -v DESCRIPTION_FILE=$DESCRIPTION_FILE \
#           -v NUM_SEASONS_FILE=$NUM_SEASONS_FILE -v NUM_EPISODES_FILE=$NUM_EPISODES_FILE \
#           -v EPISODE_CURL_FILE=$EPISODE_CURL_FILE -v IN_CANADA=$IN_CANADA \
#           -v EPISODE_INFO_FILE=$EPISODE_INFO_FILE -v SERIES_NUMBER=$lastRow \
#           -v DURATION_FILE=$DURATION_FILE -v ERROR_FILE=$ERROR_FILE -f getAcornFrom-seriesPages.awk
#
# INPUT:
#        <title>Acorn TV | 19-2</title>
#  ---
#        19-2
#        <meta itemprop="numberOfEpisodes" content="30" />
#        <meta itemprop="numberOfSeasons" content="3" />
#  ---
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#  ---
#
"The writing is sublime" (New \
#        York Times) in this anything-but-a-procedural cop drama ... as reluctant partners \
#        patrolling the streets of Montreal. Not Available in Canada. CC Available.

#  ---
#
We're sorry, but Acorn TV is not available in this territory.

#  ---
#        <meta itemprop="seasonNumber" content="1" />
#  ---
#
#  ---
#        <meta itemprop="timeRequired" content="T44M06S" />
#  ---
#
Partners

#  ---
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

#
#
#
#  OUTPUT:
#       $EPISODE_INFO_FILE
#

# Extract the series title
/<title>/ {
    sub (/.*<title>Acorn TV \| /,"")
    sub (/<.*/,"")
    gsub (/&/,"\\&")
    # gsub (/'/,"'")
    # gsub (/"/,"\"\"")
    if (match ($0, /^The /)) {
        $0 = substr($0, 5) ", The"
    }
    episodeLinesFound = 0
    numEpisodesStr = ""
    seriesTitle = $0
    print seriesTitle >> TITLE_FILE
    next
}

# Extract the number of episodes in the series
/itemprop="numberOfEpisodes"/ {
    split ($0,fld,"\"")
    episodeLinesFound += 1
    if (episodeLinesFound == 1)
        totalEpisodes = fld[4]
    if (episodeLinesFound != 1)
        numEpisodesStr = numEpisodesStr "+" fld[4]
    next
}

# Extract the number of seasons in the series
/itemprop="numberOfSeasons"/ {
    split ($0,fld,"\"")
    numSeasons = fld[4]
    if (numSeasons == 0)
        printf ("==> No seasons: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
    print numSeasons >> NUM_SEASONS_FILE
    next
}

/Acorn TV is not available in this territory/ {
    printf ("==> Not available here: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
}

# Extract the series description
/id="franchise-description"/ {
    # get rid of boilerplate
    split ($0,fld,"[<>]")
    description = fld[3]
    # get rid of unnecessary characters and text
    gsub (/\\/,"",description)
    if (IN_CANADA != "yes") {
        sub (/Series [[:digit:]]* not available in Canada\./,"",description)
        sub (/Not [Aa]vailable in Canada\./,"",description)
        sub (/NOT AVAILABLE IN CANADA\./,"",description)
    }
    sub (/CC Available\. CC Available/,"CC Available",description)
    # fix sloppy input spacing
    sub (/\.CC Available/,". CC Available",description)
    gsub (/ \./,".",description)
    gsub (/  */," ",description)
    sub (/^ */,"",description)
    sub (/ *$/,"",description)
    # fix funky HTML characters
    gsub (/'/,"'",description)
    print description >> DESCRIPTION_FILE
    next
}

# Extract the seasonNumber
# Note that Acorn "season" numbers may not correspond to actual season numbers.
# They simply refer to the number of seasons available on Acorn.
/<meta itemprop="seasonNumber" content="/ {
    split ($0,fld,"\"")
    seasonNumber = fld[4]
    next
}

# Extract the episode URL
/> EPISODE_CURL_FILE
    next
}

# Extract the episode duration
/<meta itemprop="timeRequired"/ {
    split ($0,fld,"\"")
    split (fld[4],tm,/[TMS]/)
    secs = tm[3]
    mins = tm[2] + int(secs / 60)
    hrs =  int(mins / 60)
    secs %= 60; mins %= 60
    #
    seriesSecs += secs
    seriesMins += mins + int(seriesSecs / 60)
    seriesHrs += hrs + int(seriesMins / 60)
    seriesSecs %= 60; seriesMins %= 60
    #
    totalSecs += secs
    totalMins += mins + int(totalSecs / 60)
    totalHrs += hrs + int(totalMins / 60)
    totalSecs %= 60; totalMins %= 60
    #
    episodeDuration = sprintf ("%02d:%02d:%02d", hrs, mins, secs)
    if (episodeDuration == "00:00:00")
        printf ("==> No duration: %d\t%s  %s\n", SERIES_NUMBER, seriesTitle, episodeURL)  >> ERROR_FILE
    next
}

# Extract the episode title
/
/ {
    split ($0,fld,"[<>]")
    episodeTitle = fld[3]
    gsub (/&/,"\\&", episodeTitle)
    next
}

# There are a number of variants in the "episodeNumber" string
#
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

# Extract episode number
/
.*span itemprop="episodeNumber">/ {
    # If show is a Prequel, use "P"
    if (episodeURL ~ /\/prequel/)
        showType = "P"
    # If show is a Miniseries, use "M"
    if (episodeURL ~ /\/miniseries/)
        showType = "M"
    # If we don't know what kind of show it is, use "S"
    if (showType == "")
        showType = "S"
    # Default episodeType to "E"
    episodeType = "E"
    # Default bonus, parttwo, and christmasspecials episodeType to "X"
    if (episodeURL ~ /bonus\/|[0-9]{1,2}parttwo\/|christmasspecial\//)
        episodeType = "X"
    # jamaicainn & newworlds bonus seasonNumber should be 1
    if (episodeURL ~ /\/jamaicainn\/bonus\/|\/newworlds\/bonus\//) {
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber = 1
    }
    # Plain christmasspecial, seriesfinale don't increment seasonNumber
    if (episodeURL ~ /\/christmasspecial\/|\/seriesfinale\//) {
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber -= 1
    }
    #
    split ($0,fld,"[<>]")
    episodeNumber = fld[5]
    # Feature films don't have episodes
    if (episodeURL ~ /\/featurefilm\//)
        next
    # neither should single episode series
    if (totalEpisodes == 1)
        next
    # The season number should match that in the URL
    # Birds of a Feather, Doc Martin, Murdoch, Poirot, Rebus, Vera, and others have problems
    if (episodeURL ~ \
           /\/series[0-9]{1,2}\/|[0-9]{1,2}bonus\/|[0-9]{1,2}parttwo\/|\/murdoch\/season[0-9]{1,2}/) {
        split (episodeURL, part, "/")
        URLseasonNumber = part[5]
        sub (/bonus/,"",URLseasonNumber)
        # Foyle's War has sets: foyleswar/series9set8bonus/
        sub (/set[0-9]/,"",URLseasonNumber)
        # Midsomer Murders has two parts: midsomermurders/series19parttwo/
        sub (/parttwo/,"",URLseasonNumber)
        sub (/christmasspecial/,"",URLseasonNumber)
        sub (/[[:alpha:]]*/,"",URLseasonNumber)
        if (URLseasonNumber != seasonNumber) {
            printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
            seasonNumber = URLseasonNumber
        }
    }
    printf ("%d\t=HYPERLINK(\"%s\";\"%s, %s%02d%s%02d, %s\"\)\t\t\t%s\n", \
        SERIES_NUMBER, episodeURL, seriesTitle, showType, seasonNumber, episodeType, episodeNumber, \
        episodeTitle, episodeDuration) >> EPISODE_INFO_FILE
    showType = ""
    next
}

/-- Viewers Also Watched --/ {
    print (episodeLinesFound == 1 ? "=0" : "=" numEpisodesStr) >> NUM_EPISODES_FILE
    if (description == "")
        printf ("==> No description: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
    description = ""
    #
    seriesDuration = sprintf ("%02d:%02d:%02d", seriesHrs, seriesMins, seriesSecs)
    print seriesDuration >> DURATION_FILE
    if (seriesDuration == "00:00:00")
        printf ("==> No duration: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
    seriesSecs = 0
    seriesMins = 0
    seriesHrs = 0
    seriesDuration = ""
    #
    SERIES_NUMBER += 1
}

END {
    # Return the total duration of all series
    printf ("%02d:%02d:%02d\n", totalHrs, totalMins, totalSecs)
}

# Create column files with lists of series titles, descriptions, number
# of seasons, and number of episodes, to be pasted into a spreadsheet.
# Note that IN_CANADA affects processing.
#
# Create a separate file with a line for each episode containing
# seriesNumber, episodeURL, seriesTitle, seasonNumber, episodeNumber,
# episodeTitle, & episodeDuration with the same columns as the primary
# spreadsheet so they can be combined into one.
#
# Create a separate file with a line for each series duration
#
# Return the total duration of all series
#
# INVOCATION:
#       curl -s https://acorn.tv/192/ \
#           | awk -v TITLE_FILE=$TITLE_FILE -v DESCRIPTION_FILE=$DESCRIPTION_FILE \
#           -v NUM_SEASONS_FILE=$NUM_SEASONS_FILE -v NUM_EPISODES_FILE=$NUM_EPISODES_FILE \
#           -v EPISODE_CURL_FILE=$EPISODE_CURL_FILE -v IN_CANADA=$IN_CANADA \
#           -v EPISODE_INFO_FILE=$EPISODE_INFO_FILE -v SERIES_NUMBER=$lastRow \
#           -v DURATION_FILE=$DURATION_FILE -v ERROR_FILE=$ERROR_FILE -f getAcornFrom-seriesPages.awk
#
# INPUT:
#        <title>Acorn TV | 19-2</title>
#  ---
#        19-2
#        <meta itemprop="numberOfEpisodes" content="30" />
#        <meta itemprop="numberOfSeasons" content="3" />
#  ---
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#  ---
#
"The writing is sublime" (New \
#        York Times) in this anything-but-a-procedural cop drama ... as reluctant partners \
#        patrolling the streets of Montreal. Not Available in Canada. CC Available.

#  ---
#
We're sorry, but Acorn TV is not available in this territory.

#  ---
#        <meta itemprop="seasonNumber" content="1" />
#  ---
#
#  ---
#        <meta itemprop="timeRequired" content="T44M06S" />
#  ---
#
Partners

#  ---
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

#
#
#
#  OUTPUT:
#       $EPISODE_INFO_FILE
#

# Extract the series title
/<title>/ {
    sub (/.*<title>Acorn TV \| /,"")
    sub (/<.*/,"")
    gsub (/&/,"\\&")
    # gsub (/'/,"'")
    # gsub (/"/,"\"\"")
    if (match ($0, /^The /)) {
        $0 = substr($0, 5) ", The"
    }
    episodeLinesFound = 0
    numEpisodesStr = ""
    seriesTitle = $0
    print seriesTitle >> TITLE_FILE
    next
}

# Extract the number of episodes in the series
/itemprop="numberOfEpisodes"/ {
    split ($0,fld,"\"")
    episodeLinesFound += 1
    if (episodeLinesFound == 1)
        totalEpisodes = fld[4]
    if (episodeLinesFound != 1)
        numEpisodesStr = numEpisodesStr "+" fld[4]
    next
}

# Extract the number of seasons in the series
/itemprop="numberOfSeasons"/ {
    split ($0,fld,"\"")
    numSeasons = fld[4]
    if (numSeasons == 0)
        printf ("==> No seasons: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
    print numSeasons >> NUM_SEASONS_FILE
    next
}

/Acorn TV is not available in this territory/ {
    printf ("==> Not available here: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
}

# Extract the series description
/id="franchise-description"/ {
    # get rid of boilerplate
    split ($0,fld,"[<>]")
    description = fld[3]
    # get rid of unnecessary characters and text
    gsub (/\\/,"",description)
    if (IN_CANADA != "yes") {
        sub (/Series [[:digit:]]* not available in Canada\./,"",description)
        sub (/Not [Aa]vailable in Canada\./,"",description)
        sub (/NOT AVAILABLE IN CANADA\./,"",description)
    }
    sub (/CC Available\. CC Available/,"CC Available",description)
    # fix sloppy input spacing
    sub (/\.CC Available/,". CC Available",description)
    gsub (/ \./,".",description)
    gsub (/  */," ",description)
    sub (/^ */,"",description)
    sub (/ *$/,"",description)
    # fix funky HTML characters
    gsub (/'/,"'",description)
    print description >> DESCRIPTION_FILE
    next
}

# Extract the seasonNumber
# Note that Acorn "season" numbers may not correspond to actual season numbers.
# They simply refer to the number of seasons available on Acorn.
/<meta itemprop="seasonNumber" content="/ {
    split ($0,fld,"\"")
    seasonNumber = fld[4]
    next
}

# Extract the episode URL
/> EPISODE_CURL_FILE
    next
}

# Extract the episode duration
/<meta itemprop="timeRequired"/ {
    split ($0,fld,"\"")
    split (fld[4],tm,/[TMS]/)
    secs = tm[3]
    mins = tm[2] + int(secs / 60)
    hrs =  int(mins / 60)
    secs %= 60; mins %= 60
    #
    seriesSecs += secs
    seriesMins += mins + int(seriesSecs / 60)
    seriesHrs += hrs + int(seriesMins / 60)
    seriesSecs %= 60; seriesMins %= 60
    #
    totalSecs += secs
    totalMins += mins + int(totalSecs / 60)
    totalHrs += hrs + int(totalMins / 60)
    totalSecs %= 60; totalMins %= 60
    #
    episodeDuration = sprintf ("%02d:%02d:%02d", hrs, mins, secs)
    if (episodeDuration == "00:00:00")
        printf ("==> No duration: %d\t%s  %s\n", SERIES_NUMBER, seriesTitle, episodeURL)  >> ERROR_FILE
    next
}

# Extract the episode title
/
/ {
    split ($0,fld,"[<>]")
    episodeTitle = fld[3]
    gsub (/&/,"\\&", episodeTitle)
    next
}

# There are a number of variants in the "episodeNumber" string
#
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

# Extract episode number
/
.*span itemprop="episodeNumber">/ {
    # If show is a Prequel, use "P"
    if (episodeURL ~ /\/prequel/)
        showType = "P"
    # If show is a Miniseries, use "M"
    if (episodeURL ~ /\/miniseries/)
        showType = "M"
    # If we don't know what kind of show it is, use "S"
    if (showType == "")
        showType = "S"
    # Default episodeType to "E"
    episodeType = "E"
    # Default bonus & parttwo episodeType to "X"
    if (episodeURL ~ /bonus\/|[0-9]{1,2}parttwo\//)
        episodeType = "X"
    # Default christmasspecial & seriesfinale episodeType to "X" & don't increment seasonNumber
    if (episodeURL ~ /christmasspecial\/|\/seriesfinale\//) {
        episodeType = "X"
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber -= 1
    }
    #
    split ($0,fld,"[<>]")
    episodeNumber = fld[5]
    # Feature films don't have episodes
    if (episodeURL ~ /\/featurefilm\//)
        next
    # neither should single episode series
    if (totalEpisodes == 1)
        next
    # The season number should match that in the URL
    # Birds of a Feather, Doc Martin, Murdoch, Poirot, Rebus, Vera, and others have problems
    if (episodeURL ~ \
           /\/series[0-9]{1,2}\/|[0-9]{1,2}bonus\/|[0-9]{1,2}parttwo\/|\/murdoch\/season[0-9]{1,2}/) {
        split (episodeURL, part, "/")
        URLseasonNumber = part[5]
        sub (/bonus/,"",URLseasonNumber)
        # Foyle's War has sets: foyleswar/series9set8bonus/
        sub (/set[0-9]/,"",URLseasonNumber)
        # Midsomer Murders has two parts: midsomermurders/series19parttwo/
        sub (/parttwo/,"",URLseasonNumber)
        sub (/christmasspecial/,"",URLseasonNumber)
        sub (/[[:alpha:]]*/,"",URLseasonNumber)
        if (URLseasonNumber != seasonNumber) {
            printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
            seasonNumber = URLseasonNumber
        }
    }
    printf ("%d\t=HYPERLINK(\"%s\";\"%s, %s%02d%s%02d, %s\"\)\t\t\t%s\n", \
        SERIES_NUMBER, episodeURL, seriesTitle, showType, seasonNumber, episodeType, episodeNumber, \
        episodeTitle, episodeDuration) >> EPISODE_INFO_FILE
    showType = ""
    next
}

/-- Viewers Also Watched --/ {
    print (episodeLinesFound == 1 ? "=0" : "=" numEpisodesStr) >> NUM_EPISODES_FILE
    if (description == "")
        printf ("==> No description: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
    description = ""
    #
    seriesDuration = sprintf ("%02d:%02d:%02d", seriesHrs, seriesMins, seriesSecs)
    print seriesDuration >> DURATION_FILE
    if (seriesDuration == "00:00:00")
        printf ("==> No duration: %d\t%s\n", SERIES_NUMBER, seriesTitle)  >> ERROR_FILE
    seriesSecs = 0
    seriesMins = 0
    seriesHrs = 0
    seriesDuration = ""
    #
    SERIES_NUMBER += 1
}

END {
    # Return the total duration of all series
    printf ("%02d:%02d:%02d\n", totalHrs, totalMins, totalSecs)
}


Semicolons in xcodebuild.awk

Semicolons in xcodebuild.awk

{
    print;
    fflush(stdout);
}

/[0-9]+: (error|warning):/ {
    errors = errors $0 "\n";
}

/(TEST|BUILD) FAILED/ {
    if (length(errors) > 0) {
        print "\n*** All errors:\n" errors;
    }

    fflush(stdout);

    # SYS_SOFTWARE
    exit 70;
}

{
    print
    fflush(stdout)
}

/[0-9]+: (error|warning):/ {
    errors = errors $0 "\n"
}

/(TEST|BUILD) FAILED/ {
    if (length(errors) > 0) {
        print "\n*** All errors:\n" errors
    }

    fflush(stdout)

    # SYS_SOFTWARE
    exit 70
}


Docs were failing with this escape.

Docs were failing with this escape.

Not sure why this started failing all of a sudden. Code has been the same for two years. Maybe awk upgrade somewhere?? CTR

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

#
# @author Daniel Kuppitz (http://gremlin.guru)
#
BEGIN {
  lang = null
  inCodeBlock = 0
}

/^\[source,/ {
  delimiter = 1
  split($0, a, ",")
  lang = gensub(/]$/, "", 1, a[2])
}

/^----$/ {
  if (inCodeBlock == 0) inCodeBlock = 1
  else inCodeBlock = 0
}

{ if (inCodeBlock) {
    switch (lang) {
      case "python":
        gsub(/^gremlin>/, ">>>")
        gsub(/^==>/, "")
        $0 = gensub(/processTraversal\("""(.*)""", jython, groovy)/, "\\1", 1)
        $0 = gensub("g\\.V\\(([^\\)]+)", "g.V(Bindings\\.of('id',\\1)", "g")
        print gensub("g\\.V\\(Bindings.of\\('id',(Bindings.of\\([^\\)]+\\))\\)\\)", "g.V(\\1)", "g")
        break
      default:
        print
        break
    }
  } else print
}

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

#
# @author Daniel Kuppitz (http://gremlin.guru)
#
BEGIN {
  lang = null
  inCodeBlock = 0
}

/^\[source,/ {
  delimiter = 1
  split($0, a, ",")
  lang = gensub(/]$/, "", 1, a[2])
}

/^----$/ {
  if (inCodeBlock == 0) inCodeBlock = 1
  else inCodeBlock = 0
}

{ if (inCodeBlock) {
    switch (lang) {
      case "python":
        gsub(/^gremlin>/, ">>>")
        gsub(/^==>/, "")
        $0 = gensub(/processTraversal\("""(.*)"""\, jython, groovy)/, "\\1", 1)
        $0 = gensub("g\\.V\\(([^\\)]+)", "g.V(Bindings\\.of('id',\\1)", "g")
        print gensub("g\\.V\\(Bindings.of\\('id',(Bindings.of\\([^\\)]+\\))\\)\\)", "g.V(\\1)", "g")
        break
      default:
        print
        break
    }
  } else print
}


ahrf.awk: Replace %s by %d for h{1,2,3,4,5,6}

#!/usr/bin/awk -f

BEGIN { FS = "\n"; RS = "" }

# Common shell symbols to HTML
{
	# Two backslashes for nawk(1) and OpenBSD awk(1)
	gsub(/\&/,"\\&")
	gsub(/</,"\\<")
	gsub(/>/,"\\>")
}

# h1, h2, h3, h4, h5, h6
/^[\t ]*:/ {
	match($0,":+")
	cnt = RLENGTH
	gsub(/^[\t ]*:+[\t ]*|[\t ]*:+[\t ]*$/,"")
	# length($0) would also work
	if (cnt <= 6 && $0 != "") {
		printf("<h%d>%s</h%d>\n", cnt, $0, cnt)
	}
	next
}

# Paragraph
/^[A-Za-z0-9_("]+/ {
	printf("
")
	for (p=1; p<NF; p++) {
		if ($p ~ / +$/) {
			gsub(/ +$/,"",$p)
			printf("%s
\n", $p)
		} else {
			printf("%s\n", $p)
		}
	}
	printf("%s
\n\n", $p)
	next
}

# Code
/^====+/ {
	if (NF > 2 && $NF ~ /====+/) {
		gsub(/^[\t ]*====+[\t ]*\n|\n[\t ]*====+[\t ]*$/,"")
		printf("
")
		for (c=1; c<NF; c++) {
			gsub(/^ +$/,"",$c)
			printf("%s\n", $c)
		}
		printf("%s
\n", $NF)
	}
	next
}

# List
/^[\t ]*\* +/ {
	printf("
\n")
	for (l=1; l<=NF; l++) {
		gsub(/^[\t ]*/,"",$l)
		if ($l ~ /\* +/) {
			match($l,"\\* +")
			str = substr($l,RSTART+RLENGTH)
			printf("\t
%s
\n", str)
		}
	}
	printf("
\n")
	next
}

# End links
/^[\t ]*\[[01]/ {
	printf("
\n")
	for (u=1; u<=NF; u++) {
		gsub(/^[\t ]*/,"",$u)
		if (match($u,"\\[[0-9]+\\]")) {
			# RSTART can be replaced by 1, because we removed useless tabs/spaces
			num = substr($u,RSTART,RLENGTH)
			url = substr($u,RSTART+RLENGTH+1)
			if (length(url) >= 60) {
				printf("\t
%s %.60s...
\n", num, url, url)
			} else {
				printf("\t
%s %s
\n", num, url, url)
			}
		}
	}
	printf("
\n")
	next
}

#!/usr/bin/awk -f

BEGIN { FS = "\n"; RS = "" }

# Common shell symbols to HTML
{
	# Two backslashes for nawk(1) and OpenBSD awk(1)
	gsub(/\&/,"\\&")
	gsub(/</,"\\<")
	gsub(/>/,"\\>")
}

# h1, h2, h3, h4, h5, h6
/^[\t ]*:/ {
	match($0,":+")
	cnt = RLENGTH
	gsub(/^[\t ]*:+[\t ]*|[\t ]*:+[\t ]*$/,"")
	# length($0) would also work
	if (cnt <= 6 && $0 != "") {
		printf("<h%s>%s</h%s>\n", cnt, $0, cnt)
	}
	next
}

# Paragraph
/^[A-Za-z0-9_("]+/ {
	printf("
")
	for (p=1; p<NF; p++) {
		if ($p ~ / +$/) {
			gsub(/ +$/,"",$p)
			printf("%s
\n", $p)
		} else {
			printf("%s\n", $p)
		}
	}
	printf("%s
\n\n", $p)
	next
}

# Code
/^====+/ {
	if (NF > 2 && $NF ~ /====+/) {
		gsub(/^[\t ]*====+[\t ]*\n|\n[\t ]*====+[\t ]*$/,"")
		printf("
")
		for (c=1; c<NF; c++) {
			gsub(/^ +$/,"",$c)
			printf("%s\n", $c)
		}
		printf("%s
\n", $NF)
	}
	next
}

# List
/^[\t ]*\* +/ {
	printf("
\n")
	for (l=1; l<=NF; l++) {
		gsub(/^[\t ]*/,"",$l)
		if ($l ~ /\* +/) {
			match($l,"\\* +")
			str = substr($l,RSTART+RLENGTH)
			printf("\t
%s
\n", str)
		}
	}
	printf("
\n")
	next
}

# End links
/^[\t ]*\[[01]/ {
	printf("
\n")
	for (u=1; u<=NF; u++) {
		gsub(/^[\t ]*/,"",$u)
		if (match($u,"\\[[0-9]+\\]")) {
			# RSTART can be replaced by 1, because we removed useless tabs/spaces
			num = substr($u,RSTART,RLENGTH)
			url = substr($u,RSTART+RLENGTH+1)
			if (length(url) >= 60) {
				printf("\t
%s %.60s...
\n", num, url, url)
			} else {
				printf("\t
%s %s
\n", num, url, url)
			}
		}
	}
	printf("
\n")
	next
}


Translators/BingTranslator: new API, fix #123

####################################################################
# BingTranslator.awk                                               #
####################################################################
#
# Last Updated: 18 May 2016
BEGIN { provides("bing") }

function bingInit() {
    HttpProtocol = "http://"
    HttpHost = "www.bing.com"
    HttpPort = 80
}

# Retrieve the Cookie needed.
function bingSetCookie(    cookie, group, header, url) {
    url = HttpPathPrefix "/translator"

    header = "GET " url " HTTP/1.1\n"                                   \
        "Host: " HttpHost "\n"                                          \
        "Connection: close\n"
    if (Option["user-agent"])
        header = header "User-Agent: " Option["user-agent"] "\n"

    cookie = NULLSTR
    print header |& HttpService
    while ((HttpService |& getline) > 0 && length($0) > 1) {
        match($0, /Set-Cookie: ([^;]*);/, group)
        if (group[1]) {
            cookie = cookie (cookie ?  "; " : NULLSTR) group[1]
        }
        l(sprintf("%4s bytes > %s", length($0), length($0) < 1024 ? $0 : "..."))
    }
    close(HttpService)
    Cookie = cookie
}

# TTS -- FIXME!
function bingTTSUrl(text, tl,    narrator) {
    narrator = Option["narrator"] ~ /^[AFaf]/ ? "female" : "male"
    # FIXME:
    # 1. use digraphia code (en-US) as Alpha-2 code alone doen't work
    # 2. how to pass cookies to an external player?
    return HttpProtocol HttpHost "/translator/api/language/Speak?"      \
        "locale=" tl "&text=" preprocess(text)                          \
        "&gender=" narrator "&media=audio/mp3"
}

function bingWebTranslateUrl(uri, sl, tl, hl) {
    return "http://www.microsofttranslator.com/bv.aspx?"        \
        "from=" sl "&to=" tl "&a=" uri
}

# Send an HTTP POST request and get response from Bing Translator.
function bingPost(text, sl, tl, hl,
                  ####
                  content, contentLength, group,
                  header, isBody, reqBody, url) {
    reqBody = "[{" parameterize("text") ":" parameterize(text) "}]"
    contentLength = dump(reqBody, group)

    url = HttpPathPrefix "/translator/api/Translate/TranslateArray?"    \
        "from=" sl "&to=" tl

    header = "POST " url " HTTP/1.1\n"                  \
        "Host: " HttpHost "\n"                          \
        "Connection: close\n"                           \
        "Content-Length: " contentLength "\n"           \
        "Content-Type: application/json\n"     # must!
    if (Option["user-agent"])
        header = header "User-Agent: " Option["user-agent"] "\n"
    if (Cookie)
        header = header "Cookie: " Cookie "\n" # must!

    content = NULLSTR; isBody = 0
    print (header "\n" reqBody) |& HttpService
    while ((HttpService |& getline) > 0) {
        if (isBody)
            content = content ? content "\n" $0 : $0
        else if (length($0) <= 1)
            isBody = 1
        l(sprintf("%4s bytes > %s", length($0), $0))
    }
    close(HttpService)

    return assert(content, "[ERROR] Null response.")
}

# Dictionary API (via HTTP GET).
function bingRequestUrl(text, sl, tl, hl) {
    return HttpPathPrefix "/translator/api/Dictionary/Lookup?"  \
        "from=" sl "&to=" tl "&text=" preprocess(text)
}

# Get the translation of a string.
function bingTranslate(text, sl, tl, hl,
                       isVerbose, toSpeech, returnPlaylist, returnIl,
                       ####
                       r,
                       content, tokens, ast,
                       _sl, _tl, _hl, il,
                       translation,
                       wShowOriginal, wShowTranslation, wShowLanguages,
                       group, temp) {
    if (!getCode(tl)) {
        # Check if target language is supported
        w("[WARNING] Unknown target language code: " tl)
    } else if (isRTL(tl)) {
        # Check if target language is R-to-L
        if (!FriBidi)
            w("[WARNING] " getName(tl) " is a right-to-left language, but FriBidi is not found.")
    }
    _sl = getCode(sl); if (!_sl) _sl = sl
    _tl = getCode(tl); if (!_tl) _tl = tl
    _hl = getCode(hl); if (!_hl) _hl = hl

    # Hot-patches for Bing's own translator language codes
    # See: <https://msdn.microsoft.com/en-us/library/hh456380.aspx>
    if (_sl == "auto")  _sl = "-"
    if (_sl == "bs")    _sl = "bs-Latn" # 'bs' is not recognized as valid code
    if (_sl == "zh-CN") _sl = "zh-CHS"
    if (_sl == "zh-TW") _sl = "zh-CHT"
    if (_tl == "bs")    _tl = "bs-Latn"
    if (_tl == "zh-CN") _tl = "zh-CHS"
    if (_tl == "zh-TW") _tl = "zh-CHT"

    bingSetCookie() # must!

    content = bingPost(text, _sl, _tl, _hl)
    tokenize(tokens, content)
    parseJson(ast, tokens)

    l(content, "content", 1, 1)
    l(tokens, "tokens", 1, 0, 1)
    l(ast, "ast")
    if (!isarray(ast) || !anything(ast)) {
        e("[ERROR] Oops! Something went wrong and I can't translate it for you :(")
        ExitCode = 1
        return
    } else if (ast[0 SUBSEP "Message"]) {
        e("[ERROR] " unparameterize(ast[0 SUBSEP "Message"]))
        e("[ERROR] " unparameterize(ast[0 SUBSEP "Details" SUBSEP 0]))
        ExitCode = 1
        return
    }

    translation = unparameterize(ast[0 SUBSEP "items" SUBSEP 0 SUBSEP "text"])

    returnIl[0] = il = unparameterize(ast[0 SUBSEP "from"])
    if (Option["verbose"] < 0)
        return getList(il)

    # Generate output
    if (!isVerbose) {
        # Brief mode
        r = translation

    } else {
        # Verbose mode

        wShowOriginal = Option["show-original"]
        wShowTranslation = Option["show-translation"]
        wShowLanguages = Option["show-languages"]
        wShowDictionary = Option["show-dictionary"]

        if (wShowOriginal) {
            # Display: original text
            if (r) r = r RS RS
            r = r m("-- display original text")
            r = r prettify("original", s(text, il))
        }

        if (wShowTranslation) {
            # Display: major translation
            if (r) r = r RS RS
            r = r m("-- display major translation")
            r = r prettify("translation", s(translation, tl))
        }

        if (wShowLanguages) {
            # Display: source language -> target language
            if (r) r = r RS RS
            r = r m("-- display source language -> target language")
            temp = Option["fmt-languages"]
            if (!temp) temp = "[ %s -> %t ]"
            split(temp, group, /(%s|%S|%t|%T)/)
            r = r prettify("languages", group[1])
            if (temp ~ /%s/)
                r = r prettify("languages-sl", getDisplay(il))
            if (temp ~ /%S/)
                r = r prettify("languages-sl", getName(il))
            r = r prettify("languages", group[2])
            if (temp ~ /%t/)
                r = r prettify("languages-tl", getDisplay(tl))
            if (temp ~ /%T/)
                r = r prettify("languages-tl", getName(tl))
            r = r prettify("languages", group[3])
        }

        if (wShowDictionary && false) { # FIXME!
            # Dictionary API
            # Note: source language must be identified
            dicContent = getResponse(text, il, _tl, _hl)
            tokenize(dicTokens, dicContent)
            parseJson(dicAst, dicTokens) # FIXME: inefficient parser

            if (anything(dicAst)) {
                # Display: dictionary entries
                if (r) r = r RS
                r = r m("-- display dictionary entries")

                # TODO
            }
        }
    }

    if (toSpeech) {
        returnPlaylist[0]["text"] = translation
        returnPlaylist[0]["tl"] = tl
    }

    return r
}

####################################################################
# BingTranslator.awk                                               #
####################################################################
#
# Last Updated: 11 Mar 2016
# http://ssl.microsofttranslator.com/dynamic/226010/js/LandingPage.js
BEGIN { provides("bing") }

function genRTTAppId(    content, group, header, isBody) {
    HttpProtocol = "http://"
    HttpHost = "ssl.microsofttranslator.com"
    HttpPort = 80
    LandingPage = "/dynamic/226010/js/LandingPage.js"

    if (Option["proxy"]) {
        match(Option["proxy"], /^(http:\/*)?([^\/]*):([^\/:]*)/, HttpProxySpec)
        HttpService = "/inet/tcp/0/" HttpProxySpec[2] "/" HttpProxySpec[3]
        HttpPathPrefix = HttpProtocol HttpHost
    } else {
        HttpService = "/inet/tcp/0/" HttpHost "/" HttpPort
        HttpPathPrefix = ""
    }

    header = "GET " LandingPage " HTTP/1.1\n"                           \
        "Host: " HttpHost "\n"                                          \
        "Connection: close\n"
    if (Option["user-agent"])
        header = header "User-Agent: " Option["user-agent"] "\n"

    content = NULLSTR; isBody = 0
    print header |& HttpService
    while ((HttpService |& getline) > 0) {
        if (isBody)
            content = content ? content "\n" $0 : $0
        else if (length($0) <= 1)
            isBody = 1
        l(sprintf("%4s bytes > %s", length($0), length($0) < 1024 ? $0 : "..."))
    }
    close(HttpService)

    match(content, /rttAppId:"([^"]+)"/, group)
    if (group[1]) {
        RTTAppId = group[1]
    } else {
        e("[ERROR] Oops! Something went wrong and I can't translate it for you :(")
        exit 1
    }
}

function bingInit() {
    genRTTAppId() # generate a one-time key

    HttpProtocol = "http://"
    HttpHost = "api.microsofttranslator.com"
    HttpPort = 80
}

function bingRequestUrl(text, sl, tl, hl) {
    # Quick hack: Bing doesn't have an "auto" language code
    if (sl == "auto") sl = NULLSTR

    return HttpPathPrefix "/v2/ajax.svc/TranslateArray2?"               \
        "appId="  preprocess(parameterize(RTTAppId, "\""))              \
        "&from="  preprocess(parameterize(sl, "\""))                    \
        "&to="    preprocess(parameterize(tl, "\""))                    \
        "&texts=" preprocess("[" parameterize(text, "\"") "]")
}

function bingTTSUrl(text, tl,    narrator) {
    narrator = Option["narrator"] ~ /^[AFaf]/ ? "female" : "male"

    return HttpProtocol HttpHost "/v2/http.svc/speak?" "appId=" RTTAppId \
        "&language=" tl "&text=" preprocess(text)                       \
        "&format=audio/mp3" "&options=MinSize|" narrator
}

function bingWebTranslateUrl(uri, sl, tl, hl) {
    return "http://www.microsofttranslator.com/bv.aspx?"        \
        "from=" sl "&to=" tl "&a=" uri
}

# Get the translation of a string.
function bingTranslate(text, sl, tl, hl,
                       isVerbose, toSpeech, returnPlaylist, returnIl,
                       ####
                       r,
                       content, tokens, ast,
                       _sl, _tl, _hl, il,
                       translation,
                       wShowOriginal, wShowTranslation, wShowLanguages,
                       group, temp) {
    if (!getCode(tl)) {
        # Check if target language is supported
        w("[WARNING] Unknown target language code: " tl)
    } else if (isRTL(tl)) {
        # Check if target language is R-to-L
        if (!FriBidi)
            w("[WARNING] " getName(tl) " is a right-to-left language, but FriBidi is not found.")
    }
    _sl = getCode(sl); if (!_sl) _sl = sl
    _tl = getCode(tl); if (!_tl) _tl = tl
    _hl = getCode(hl); if (!_hl) _hl = hl

    content = getResponse(text, _sl, _tl, _hl)
    # Strip the content and get a valid JSON string
    match(content, /(\[.*\])$/, group)
    if (!group[0]) {
        # Display the error message
        match(content, /"(.*)"$/, group)
        gsub(/\\u000d/, "\r", group[1])
        gsub(/\\u000a/, "\n", group[1])
        e("[ERROR] " group[1])
        ExitCode = 1
        return
    }
    content = group[1]
    tokenize(tokens, content)
    parseJson(ast, tokens)

    l(content, "content", 1, 1)
    l(tokens, "tokens", 1, 0, 1)
    l(ast, "ast")
    if (!isarray(ast) || !anything(ast)) {
        e("[ERROR] Oops! Something went wrong and I can't translate it for you :(")
        ExitCode = 1
        return
    }

    translation = unparameterize(ast[0 SUBSEP 0 SUBSEP "TranslatedText"])

    returnIl[0] = il = unparameterize(ast[0 SUBSEP 0 SUBSEP "From"])
    if (Option["verbose"] < 0)
        return getList(il)

    # Generate output
    if (!isVerbose) {
        # Brief mode
        r = translation

    } else {
        # Verbose mode
        wShowOriginal = Option["show-original"]
        wShowTranslation = Option["show-translation"]
        wShowLanguages = Option["show-languages"]

        if (wShowOriginal) {
            # Display: original text
            if (r) r = r RS RS
            r = r m("-- display original text")
            r = r prettify("original", s(text, il))
        }

        if (wShowTranslation) {
            # Display: major translation
            if (r) r = r RS RS
            r = r m("-- display major translation")
            r = r prettify("translation", s(translation, tl))
        }

        if (wShowLanguages) {
            # Display: source language -> target language
            if (r) r = r RS RS
            r = r m("-- display source language -> target language")
            temp = Option["fmt-languages"]
            if (!temp) temp = "[ %s -> %t ]"
            split(temp, group, /(%s|%S|%t|%T)/)
            r = r prettify("languages", group[1])
            if (temp ~ /%s/)
                r = r prettify("languages-sl", getDisplay(il))
            if (temp ~ /%S/)
                r = r prettify("languages-sl", getName(il))
            r = r prettify("languages", group[2])
            if (temp ~ /%t/)
                r = r prettify("languages-tl", getDisplay(tl))
            if (temp ~ /%T/)
                r = r prettify("languages-tl", getName(tl))
            r = r prettify("languages", group[3])
        }
    }

    if (toSpeech) {
        returnPlaylist[0]["text"] = translation
        returnPlaylist[0]["tl"] = tl
    }

    return r
}