#!/usr/bin/perl
use Data::Dumper;
use Getopt::Long;
use File::Basename qw( fileparse );
use File::Path qw( make_path );
use Digest::SHA qw(sha256_hex);
use Fcntl qw(:flock);
use POSIX qw( strftime );
use File::stat;
use JSON;

use open ":std", ":encoding(UTF-8)";

my $output;
my $opt_target = 'main';
my $opt_lang;
my $opt_debug;
my $opt_verbose;
my $opt_output_file;
my $opt_html;
my $opt_latex;
my $opt_loc;
my $opt_draft;
my $opt_daemon; # 1 -- run server; 2: run server in background
my $opt_live; # true -- live rendering in server mode
my $opt_toc;
my $opt_port = 8080;
my $opt_source_map;
my $opt_json_dump;
my $opt_help;

my @section_levels = ('part', 'chapter', 'section', 'subsection', 'subsubsection', 'paragraph', 'subparagraph');
my $opt_document_class = 'book';
my $opt_top_heading;
my %global_labels = ();
my $global_label_id = 1;
my $re_ln_label = qr/\{#(\w+)}\s*$/;
my $global_vars = {};
my @env_chain = ();
my $re_defvar = qr/^[@-]\s*\{([A-Za-z0-9_ -]+)}=\s+`(.*?)`/;
my $re_command = qr/@(mark|table|c|\+)\s*\b(.*)/;
my $re_inline_var = qr/`?\{([#=][A-Za-z].*?)}/;
my %re_relaxed_inline_var = (
  #c => qr/`?\{([#=]?[A-Za-z].*?)}/
);

my $g_curr_lang;
my %loc_formats = (
 c    => sub { my ($file,$ln) = @_; return "#line $ln \"$file\"\n"; },
 cpp  => sub { my ($file,$ln) = @_; return "#line $ln \"$file\"\n"; },
 nasm => sub { my ($file,$ln) = @_; return "%line $ln \"$file\"\n"; },
);
my %latex_listing_supported_languages = (c=>1,perl=>1,cpp=>1,html=>1,java=>1);
sub sections_to_json {
  my $sections = shift;

  eval("require JSON") or do {
    print STDERR "sudo cpan JSON\n";
    die "Can not load JSON.pm";
  };

  my @a = ();
  for (@{$sections}) {
    my $section = $_;
    my $numLines = scalar @{$section->{lines}};
    my $ln = $numLines > 0 ? $section->{lines}[0]->{ln} : 0;
    my @lines = (map { $_->{text} } @{$section->{lines}});
    my @blocks = (map {
      my $blk = $_;
      my $numLines = 2 + (scalar @{$blk->{lines}});
      {
        type      => $blk->{type},
        ln        => $blk->{ln},
        lang      => $blk->{lang},
        num_lines => $numLines,
      };
    } @{$section->{blocks}});

    push @a, {
      id      => $section->{id},
      type    => $section->{type},
      heading => $section->{heading},
      hidden  => $section->{hidden},
      fields  => $section->{fields},
      file    => $section->{file},
      ln      => $ln,
      lines   => \@lines,
      blocks  => \@blocks,
    };
  }
  return JSON::encode_json({
    version => 1,
    rev => 1,
    instance_id => 'i-'.gen_uuid(),
    sections => \@a
  });
}
use integer; # otherwise it's float point, and won't work at all for large integer!

my $sortable_b64_alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_abcdefghijklmnopqrstuvwxyz";

sub sortable_b64_encode_int {
  my $integ = int(shift);
  my $l = shift;
  my $alphabet = $sortable_b64_alphabet;

  my $encoded = '';
  my $base = length($alphabet);
  my @digits = ();
  my $n = $integ;

  while ($n > 0) {
    my $remainder = $n % $base;
    $encoded = substr($alphabet, $remainder, 1) . $encoded;
    $n = int($n / $base);
    push @digits, $remainder;
  }
  return substr($alphabet, 0, 1) x ($l - length($encoded)) . $encoded;
}

#use UUID 'uuid';
use Time::HiRes qw(time);

my $uuid_seq = 1;
sub gen_uuid {
  my $ms = int(time()*1000);
  my $r =  int(rand(64));
#  my $id = ($ms << 18) + ($uuid_seq << 6) + $r;
 #my $id =  ($uuid_seq << 6) + $r;
 my $id = ($ms << 18) + ($uuid_seq << 6) + $r;
  $uuid_seq = ($uuid_seq + 1) % 4096;
  return sortable_b64_encode_int($id, 12),
}



%options = (
  "--loc"               => [\$opt_loc, 'Generate code location in the output'],
  "--debug"             => [\$opt_debug, 'Enable debug'],
  "--live"              => [\$opt_live, 'Render for httpd server'],
  "--draft"             => [\$opt_draft, 'Generate draft for latex'],
  "--lang LANG"            => [\$opt_lang, 'Program language for output source '],
  "--target SECTION"          => [\$opt_target, 'starting section'],
  "--section SECTION"         => [\$opt_target, 'starting section'],
  "--exec SECTION"            => [\$opt_exec, 'execute section'],
  "--toc"               => [\$opt_toc, 'generate toc'],
  "--html"              => [\$opt_html, 'Set output format for documentation'],
  "--latex"             => [\$opt_latex, 'Set output format for documentation'],
  "--source-map FILENAME"      => [\$opt_source_map, 'Generate source map'],
  "--top-heading LEVEL"     => [\$opt_top_heading, 'Set top level section type: <part|chapter|section>'],
  "--document-class CLASS"  => [\$opt_document_class, 'Set latex document class'],
  "--verbose"           => [\$opt_verbose, 'More logging'],
  "--output FILENAME"          => [\$opt_output_file, 'Set output file'],
  "--port PORT:i"            => [\$opt_port, 'Live HTTP Server port'],
  "--daemon MODE:i"          => [\$opt_daemon, "Run server in: \n1 => foreground, 2 => background"],
  "--json-dump"         => [\$opt_json_dump, 'Dump the document'],
  "-x SECTION"               => [\$opt_exec, 'short for --exec'],
  "-o OUTPUTFILE"               => [\$opt_output_file, 'short for --output'],
  "-t SECTION"               => [\$opt_target, 'short for --target'],
  "-h"                 => [\$opt_help, 'Shows help information'],
);

my %optionsSpec = map {
    my $k = $_;
    my $ref = $options{$_}[0];

    my @x = split /\s+/, $k;
    my $name = $x[0];
    $name =~ s/^-+//;
    my $suffix;
    if ($x[1]) {
      my @a = split /:/, $x[1];
      $suffix = "=".($a[1]||'s');
    }
    $name.$suffix => $ref
} keys %options;

GetOptions(%optionsSpec) or usage("Invalid options");

usage() if $opt_help || (scalar @ARGV == 0 && !$opt_daemon);

if ($opt_top_heading) {
 while ($section_levels[0] ne $opt_top_heading) {
  die "Invalid top heading: $opt_top_heading" if scalar(@section_levels) < 2;
  shift @section_levels;
 }
}
sub usage {
  my $msg = shift;
  print STDERR $msg,"\n" if $msg;
  print STDERR "zen [options] <file.zen>\n";
  foreach (sort keys %options) {
    my @desc = split(/\n/, $options{$_}[1]);
    s/:.*//;
    print STDERR sprintf("  %-24s %s\n", $_, shift @desc);
    print STDERR sprintf("  %-24s $_\n", '') foreach (@desc);
  }
  exit(1);
};
sub add_label
{
  my ($sec,$name,$ln) = @_;
  die unless $sec;
  if (exists $sec->{labels}->{$name}) {
    die "$sec->{file}:$ln: duplicated labels in same section";
  }
  my $lab = {
    id => 'L-'.($global_label_id++),
    section => $sec,
    file =>$sec->{file},
    ln => $ln
  };
  $sec->{labels}->{$name} = $lab;
  return $lab;
}
sub add_global_label
{
  my ($sec,$name,$ln) = @_;
  if (exists $global_labels{$name}) {
    die "$sec->{file}:$ln: duplicated label\n";
  }
  my $x = {
    id => 'L-' . ($global_label_id++),
    section => $sec,
    file => $sec->{file},
    ln => $ln
  };
  $global_labels{$name} = $x;
  return $x;
}

sub find_label
{
  my ($name, $sec) = @_;
  if ($sec && exists($sec->{labels}->{$name})) {
    return $sec->{labels}->{$name};
  }
  if (exists $global_labels{$name}) {
    return $global_labels{$name};
  }
}

sub lookup {
  my ($name) = @_;
  foreach (@env_chain) {
    if (exists $_->{$name}) {
      return $_->{$name};
    }
  }
  return $global_vars->{$name} if exists $global_vars->{$name};
  return $ENV{$name} if exists $ENV{$name};
  err("lookup not found: '$name'");
}
#print lookup("HOME", [ "b",{ x1 => 1 },"a"]),"\n";

sub enter {
   my $loc = shift;
   my $env = { __loc => $loc };
   unshift @env_chain, $env;
   return $env;
}

sub leave {
  die "Env chain empty" if scalar(@env_chain)==0;
  shift @env_chain;
}

my @sections = ();

sub find_section
{
  my ($s,$ignore) = @_;
  my $ret;
  my $prefix;
  $prefix = $1 if ($s =~ m/(.+)\.\.\.$/);
  foreach (@sections) {
    my $section = $_;

    if ($prefix) {
      next if index($section->{heading}, $prefix) != 0;
    } elsif ($s ne $section->{heading}) {
      my $re = $section->{pattern};
      next unless ($s =~ m/^$re$/);
    }

    if ($ret) {
      err("find section: '$s' has two matches\n"
        . "  $section->{file}:$section->{ln}: $section->{heading}\n"
        . "  $ret->{file}:$ret->{ln}: $ret->{heading}");
    } else {
      $ret = $section;
    }
  }
  err("No section found for `$s'") unless ($ignore || $ret);
  return $ret;
}
sub load_zen_file
{
  my $filename = shift;
  my $st = stat($filename);
  my $mtime = $st->mtime;
  my $filesize = $st->size;

  open(my $fh, '<:encoding(UTF-8)', $filename)
    or die "Could not open file '$filename' $!";

  flock($fh, LOCK_EX) or die "Could not lock file '$filename' $!";

  my $ln = 0;
  my @lines = ();
  my $curr_section;
  my $curr_block;
  my $backticks;
  my $mark; # this can be useful to mark/label everything. to give them to everything.

  {
    my $level = 0;
    my $pattern;
    my $heading = '';
    die "$filename:$ln: previous code block not ended\n" if $curr_block;
    my $params = [];
    
    if ($pattern) {
      $pattern = trim1($pattern);
      $pattern =~ s/^#//;
      my @a = split /:/,$pattern;
      $pattern = $a[0];
      push @{$params}, split /,/,$a[1] if scalar(@a) > 1;
    }
    
    $curr_section = {
      id => 'S-'.$ln,
      type    => 'default',
      level   => $level,
      heading => $heading,
      pattern => $pattern,
      params  => $params,
      file    => $filename,
      refs    => [],
      ln      => $ln,
      defs    => [],
      labels  => {},
      blocks  => [],
      extra   => [],
      lines   => [],
      fields  => {},
      max_line_id   => 1
    };
    push @sections, $curr_section;
    if ($curr_section->{heading} =~ m/^--+\s*(.*)/) {
      $curr_section->{type} = 'inlined';
    }
    
    $curr_section->{hidden} = 1;
  }
  while (my $s = <$fh>) {
    chomp $s;
    $ln++;

    my $l = {
       section => $curr_section,
       ln   => $ln,
       file => $filename,
       text => $s
    };

    if ($curr_block) {
      push @{$curr_section->{lines}}, $l if $curr_section;
      if ($curr_block->{type} eq 'table') {
        if ($s !~ m/^\|/) {
          if ($opt_debug) {
            print STDERR "new table:";
            print Dumper $curr_block->{rows};
          }
          $curr_block = undef;
          $l->{end_block} = 1;
        } else {
          if ($l->{text} =~ m/^\|-[|-]+\|\s*$/) {
            $curr_block->{has_headers} = 1;
          } else {
            push @{$curr_block->{rows}}, [parse_table_row($l->{text})];
          }
          push @{$curr_block->{lines}}, $l;
        }
      } elsif ($backticks && $s =~ m/^$backticks\s*$/) {
        
        foreach (@{$curr_block->{lines}}) {
          if ($_->{id}) {
            $curr_block->{first_number} = $_->{id};
            last;
          }
        }
        
        xtrim(@{$curr_block->{lines}});
        
        $backticks = undef;
        $curr_block = undef;
        $l->{end_block} = 1;
      } elsif ($backticks && $s =~ m/^$backticks[^`]/) { # For detecting run away code sections (mystic errors, demystify it. for user's sake to recover from errors. Looks like unnecessary, but crucial.
        die "$filename:$ln: this is not a clean ending for code block at line $curr_block->{ln}. "
         . "Maybe you forgot to close it in previous section?\n";
      } elsif ($s =~ m/^\s*\.label\s+(\w+)/) {
        add_label($curr_section, $1, $ln)->{line} = $curr_section->{max_line_id};
        push @{$curr_block->{lines}}, $l;
      } else {
        if ($s =~ m/$re_ln_label/) {
          add_label($curr_section, $1, $ln)->{line} = $curr_section->{max_line_id};
        }
        $l->{id} = $curr_section->{max_line_id}++;
        #print STDERR "add code: $l->{ln}: $l->{text}\n";
        push @{$curr_block->{lines}}, $l;
      }
      next;
    }


    # If need be, we should also support {/    /:m,n}
    # so that we have special chars in the pattern
    if ($s =~ m/^(#{1,9})\s+(.*?)(\{#?\w.*?\})?\s*$/) {
      my $level = length($1);
      my $pattern = $3;
      my $heading = trim($2);
      die "$filename:$ln: previous code block not ended\n" if $curr_block;
      my $params = [];
      
      if ($pattern) {
        $pattern = trim1($pattern);
        $pattern =~ s/^#//;
        my @a = split /:/,$pattern;
        $pattern = $a[0];
        push @{$params}, split /,/,$a[1] if scalar(@a) > 1;
      }
      
      $curr_section = {
        id => 'S-'.$ln,
        type    => 'default',
        level   => $level,
        heading => $heading,
        pattern => $pattern,
        params  => $params,
        file    => $filename,
        refs    => [],
        ln      => $ln,
        defs    => [],
        labels  => {},
        blocks  => [],
        extra   => [],
        lines   => [],
        fields  => {},
        max_line_id   => 1
      };
      push @sections, $curr_section;
      if ($curr_section->{heading} =~ m/^--+\s*(.*)/) {
        $curr_section->{type} = 'inlined';
      }
      
      push @{$curr_section->{lines}}, $l;
      next;
    }

    push @{$curr_section->{lines}}, $l if $curr_section;


    if ($s =~ m/^(```+)(\w+)?\s+@(\[.+?]|\{.+?}|\<.+?>)?(\+=|;\+=|=)?\s*$/) {
      $backticks = $1;
      my $lang = $2;
      my $name = $3;
      my $op = $4;
      if (has_prefix($name, '[')) {
        die "$file:$ln: unexpected op" if $op ne '+=';
        $name = '+' . trim1($name);
      } elsif (has_prefix($name, '<')) {
        $name = '>' . trim1($name);
      } else {
        $name = trim1($name);
      }
      #print STDERR "Start code block: $name $lang\n";
      
      die "$filename:$ln: Previous block not closed" if $curr_block;
      
      $curr_block = {
        type => 'code',
        name => $name,
        file => $filename,
        lang => $lang,
        ln => $ln,
        lines => []
      };
      
      push @{$curr_section->{blocks}}, $curr_block;
      $l->{begin_block} = $curr_block;
    } elsif ($s =~ m/^(```+)(\w+)?\s+!(.*)/) {
      $backticks = $1;
      my $lang = $2;
      my $name;
      my $params = trim($3);
      $name = $params if $params;
      
      die "$filename:$ln: Previous block not closed" if $curr_block;
      
      $curr_block = {
        type => 'code',
        name => $name,
        file => $filename,
        lang => $lang,
        ln => $ln,
        lines => []
      };
      
      push @{$curr_section->{blocks}}, $curr_block;
      $l->{begin_block} = $curr_block;
    } elsif ($s =~ m/^\|(.*)\|\s*$/) {
      die "$filename:$ln: Previous block not closed" if $curr_block;
      $curr_block = {
        type => 'table',
        name => '',
        file => $filename,
        ln => $ln,
        lines => [],
        rows => [],
      };
      
      push @{$curr_section->{blocks}}, $curr_block;
      $l->{begin_block} = $curr_block;
      if ($l->{text} =~ m/^\|-[|-]+\|\s*$/) {
        $curr_block->{has_headers} = 1;
      } else {
        push @{$curr_block->{rows}}, [parse_table_row($l->{text})];
      }
      push @{$curr_block->{lines}}, $l;
    } elsif ($s =~ m/$re_defvar/) {
      my $name = $1;
      my $val = $2;
  	  foreach (@{$curr_section->{defs}}) {
        die "$filename:$ln: redefining $name" if $_->{name} eq $name;
	    }
      push @{$curr_section->{defs}}, {
        name => $name,
        file => $filename,
        ln => $ln,
        text => $val
      };
      #print STDERR "def $name $val\n";
    } elsif ($s =~ m/^\.(label)\b\s*(.*)/) {
      my $cmd = $1;
      my $params = trim($2);
      if ($cmd eq 'global') {
       if ($params =~ m/(\w+)\s+(.*)/) {
         my $name = $1;
         my $val = $2;
         die "$filename:$ln: redefining $name" if exists $global_vars->{$name};
         $global_vars->{$name} = expand1($2,[{_env_description=>"$filename:$ln"}]);
       } else {
         die "$filename:$ln: bad global";
       }
      } elsif ($cmd eq 'def') {
        if ($params =~ m/(\w+)\s+(.*)/) {
          my $name = $1;
          my $val = $2;
      	  foreach (@{$curr_section->{defs}}) {
      	      die "$filename:$ln: redefining $name" if $_->{name} eq $name;
      	  }
          push @{$curr_section->{defs}}, {
            name => $name,
            file => $filename,
            ln => $ln,
            text => $val
          };
        } else {
          die "$filename:$ln: bad def";
        }
      } elsif ($cmd eq 'import') {
        load_zen_file($params);
      } elsif ($cmd eq 'label') {
        add_global_label($curr_section, trim($params), $ln);
      } else {
        die "$filename:$ln: Invalid command '$cmd'";
      }
    } elsif ($s =~ m/^:(\w+):\s*(.*)/) {
      $curr_section->{fields}->{$1} = {
        value => trim($2),
        ln => $ln
      };
    }
  }
  die "Error: $curr_block->{file}:$curr_block->{ln}: block not closed\n"
     if $curr_block;

  flock($fh, LOCK_UN) or die "Could not unlock file '$filename' $!";
  close($fh);
  return {
    last_modified => $mtime,
    size => $filesize,
    sections => \@sections
  }
}
sub build_refs {
  for (@sections) {
    my $section = $_;
    foreach (@{$section->{blocks}}) {
      my $block = $_;
      my $name = $block->{name};
      if ($name =~ m/^\+(\[.*?]|.+)/) {
        my $x = $1;
        $x = trim1($x) if has_prefix($x, "[");
        my $sec = find_section($x);
        add_ref($sec, $section, 'extended') if $sec;
      }
      foreach (@{$block->{lines}}) {
        my $l = $_;
        my $s = $l->{text};
        if ($s=~m/^(.*?)\.i\s+(.*)/) {
          my $prefix = $1;
          my $included = $2;
      
          # .i could be after .when or .for
          next if !($prefix =~ m/^\s*$/)
               && !($prefix =~ m/^\s*[\.@](when|if|for)/);
      
          foreach (extract_included($included)) {
            my @a = split /:/,$_;
            my $sec = find_section($a[0], true);
            add_ref($sec, $section, 'included') if $sec;
          }
        } elsif ($s=~m/^(.*?)\[\[(.*)]]\s*$/) {
          my $prefix = $1;
          my $included = $2;
      
          # .i could be after .when or .for
          next if !($prefix =~ m/^\s*$/)
               && !($prefix =~ m/^\s*[\.@](when|if|for)/);
      
          foreach (split /;/,$included) {
            my @a = split /:/,trim($_);
            my $sec = find_section($a[0], true);
            add_ref($sec, $section, 'included') if $sec;
          }
        }
      }
    }
  }
}

sub add_ref {
  my ($sec1, $sec2, $type) = @_;
  foreach (@{$sec1->{refs}}) {
    my $ref = $_;
    return if ($ref->{section} == $sec2 && $ref->{type} eq $type);
  }
  push @{$sec1->{refs}}, {
    type => $type,
    section => $sec2
  };
}
sub expand {
  my $name = shift;
  my @args = @_;
  my $section = find_section($name);
  my $env = enter($section->{file} . ":" . $section->{ln});
  my @ret = ();

  my @params = @{$section->{params}};
  if (scalar(@params) > 0) {
   my $pat = $section->{pattern};
   my @vals = ($name =~ /^$pat$/);
   @vals = () unless defined $1;
   push @vals,@args;
   if (scalar(@params) != scalar(@vals)) {
     print STDERR "Unmatched parameters:\n",
       "  params: ", join(',',@params), "\n",
       "  values: ", join(',',@vals), "\n";
  
     err("unbound parameters") if scalar(@params) > scalar(@vals);
     err("unbound values") if scalar(@params) < scalar(@vals);
   }
   foreach (@params) {
     $env->{$_} = shift @vals;
   }
  }
  foreach (@{$section->{defs}}) {
    my $def = $_;
    my @x = expand1($def);
    my $y = shift @x;
    $env->{$def->{name}} = $y->{text};
  }
  foreach (@{$section->{blocks}}) {
    my $block = $_;
    my $name = $block->{name};
    next if !$name;
    my @lines = expand_block($block);
    if ($name =~ m/^\+(\[.*?]|.+)/) {
      my $x = $1;
      $x = trim1($x) if has_prefix($x, "[");
      my $t =find_section($x);
      push @{$t->{extra}}, @lines;
      #print STDERR "adding to $t->{heading}\n";
    } elsif ($name =~ m/^\>(\[.*?]|[A-Za-z0-9_.-\/]+)/) {
      my $x = $1;
      $x = trim1($x) if has_prefix($x, "[");
      
      my ($filename, $dir) = fileparse $x;
      if (!-d $dir) {
          make_path $dir or die "Failed to create path: $dir";
      }
      open $fh, ">$x" or die "Can not write to `$x'";
      emit($fh, $x, @lines);
      close $fh;
    } else {
      my $x = [];
      push @{$x}, @lines;
      dbg("adding named block: $name: ",scalar(@lines), " lines");
      #print STDERR "set $name \n";
      $env->{$name} = $x;
    }
  }
  foreach (@{$section->{blocks}}) {
    my $block = $_;
    my $name = $block->{name};
    next if $block->{type} ne 'code';
    next if $name;
    my @lines = expand_block($block);
    dbg("adding block: ",scalar(@lines), " lines");
    push @ret, @lines;
  }

  leave();

  push @ret, @{$section->{extra}};
  dbg("expanded: $name: ", scalar(@ret), " lines");
  return @ret;
}

sub replace {
  my $l = shift;
  my $s = $l->{text};
  my $re = $re_relaxed_inline_var{$g_curr_lang} || $re_inline_var;
  my $t = '';
  while ($s =~ m/$re/) {
    $t .= $`;
    if (substr($&,0,1) eq '`') {
      $t .= substr($&,1);
    } elsif (substr($1, 0, 1) eq '#') {
      # Skip label
    } elsif (substr($1, 0, 1) eq '=') {
      $t .= eval_expr(substr($1,1));
    } else {
      $t .= eval_expr($1);
    }
    $s = "$'";
  }
  $t .= $s;
  return { file=>$l->{file}, ln=>$l->{ln}, text=>$t };
}

my %operators = (
 'eq'    => sub { my ($x,$y) = @_; return $x eq $y; },
 'is'    => sub { my ($x,$y) = @_; return $x eq $y; },
 'ne'    => sub { my ($x,$y) = @_; return $x ne $y; },
 'not'   => sub { my ($x,$y) = @_; return $x ne $y; },
 'in'    => sub {
   my ($x,$y) = @_;
   return grep(/^$x$/, (split /,/,$y)) ? 1 : 0;
 },
 'notin' => sub {
   my ($x,$y) = @_;
   return grep(/^$x$/, (split /,/,$y)) ? 0 : 1;
 },
 'add'   => sub { my ($x,$y) = @_; return $x + $y; },
 'mul'   => sub { my ($x,$y) = @_; return $x * $y; },
 'sub'   => sub { my ($x,$y) = @_; return $x - $y; },
 'div'   => sub { my ($x,$y) = @_; return $x / $y; },
 'quoteb' => sub {
   my ($x,$y) = @_;
   $x =~ s/\\/\\\\/g;
   return $x;
  },
 'quote' => sub {
   my ($x,$y) = @_;
   $x =~ s/\\/\\\\/g;
   $x =~ s/\n/\\n/g;
   $x =~ s/\t/\\t/g;
   $x =~ s/\r/\\r/g;
   $x =~ s/"/\\"/g;
   $x =~ s/([$y])/\\\1/g if $y;
   return $x;
  },
  'uc'   => sub { my ($x) = @_; return uc($x); },
  'lc'   => sub { my ($x) = @_; return lc($x); },
);

sub eval_expr
{
  my @a = split /:/,shift;
  my $x = shift @a;
  my $y = lookup($x);
  if (ref $y eq 'ARRAY') {
    $y = join("\n", map { $_->{text} } @{$y}); 
  }
  return $y if (scalar(@a) == 0);
  my $op = shift @a;
  err("undefined op: $op") unless exists $operators{$op};
  return $operators{$op}($y,@a);
}

sub dupln {
  my ($l,$new_text) = @_;
  return {ln => $l->{ln}, file => $l->{file},
    section => $l->{section},
    text => $new_text || $l->{text}};
}
sub expand1 {
  my $l = shift;
  dbg("expand1: $l->{ln}: $l->{text}");
  my @ret = ();
  my $old = $env_chain[0]->{__loc};
  $env_chain[0]->{__loc} = "$l->{file}:$l->{ln}";
  my $s = $l->{text};

  if ($s=~m/^(\s*)\.label/) {
    # Do nothing at all
  } elsif ($s=~m/^(\s*)\.i\s+(.*)/) {
    die;
    my $prefix = $1;
    my $x = {
      ln => $l->{ln},
      file => $l->{file},
      text => $2
    };
    my @a = expand1($x);
    my $y = shift @a;
    foreach (extract_included($y->{text})) {
      my @a = split /:/,$_;
      if (scalar(@a) > 1) {
        my $z = pop @a;
        push @a, split /,/,$z;
      }
      push @ret, indent($prefix,expand(@a))
    }
  } elsif ($s =~ m/^(\s*)\[\[(.*)]]/) { # allow a trailing label
    my $prefix = $1;
    my @a = expand1(dupln($l, $2));
    my $y = shift @a;
    foreach (split /;/, $y->{text}) {
      my @a = split /:/,trim($_);
      if (scalar(@a) > 1) {
        my $z = pop @a;
        push @a, split /,/,$z;
      }
      push @ret, indent($prefix,expand(@a))
    }
  } elsif ($s=~m/^(\s*)[\.@](when|if)\s*\{(.*?)}/) {
    my $prefix = $1;
    $s = "$'";
    if (eval_expr($3)) {
      my @a = expand1(dupln($l, $s));
      push @ret, indent($prefix,@a);
    }
  } elsif ($s=~m/^(\s*)[\.@]for\s*\{(.*?)}/) {
    my $prefix = $1;
    $s = "$'";
    my ($name,$t) = split /:/,$2;
    my @vals;
    
    if ($name =~ m/^(\d+)\.\.(\d+)$/) {
      my $i = $1;
      my $j = $2;
      while ($i <= $j) {
        push @vals, $i;
        $i++;
      }
    } else {
      my $value = lookup($name);
      if (ref $value eq 'ARRAY') {
        my $t = '';
        $t .= $_->{text} foreach @{$value};
        $value = $t;
      }
      @vals = map { trim($_) } split /,/, $value;
    }
    
    my @vars = split /,/, $t;
    if (scalar(@vals) % scalar(@vars) != 0) {
      err("$name has incorrect elements");
    }
    
    while (scalar(@vals) > 0) {
      my $env = enter($l->{file} . ":" . $l->{ln});
      foreach (@vars) {
        $env->{$_} = shift @vals;
      }
      my @a = expand1(dupln($l, $s));
      push @ret, indent($prefix,@a);
      leave();
    }
  } elsif ($s =~ m/^(\s*)\{\{(.*)\}\}\s*$/) {
    my $prefix = $1;
    my $name = $2;
    #print STDERR "Looking up $name\n";
    push @ret, indent($prefix,@{lookup($name)});
  } elsif($s =~ m/^(\s*)`([@.{[].*)/) {
    push @ret, dupln($l, $1 . $2); 
  } else {
    push @ret, replace($l);
  }
  $env_chain[0]->{__loc} = $old;
  return @ret;
}
sub expand_block {
    my $block = shift;
    my $prev_lang = $g_curr_lang;
    $g_curr_lang = $block->{lang} || $opt_lang || '';
    my @lines = ();
    foreach (@{$block->{lines}}) {
       push @lines, expand1($_);
    }
    $g_curr_lang = $prev_lang if $prev_lang; 
    return @lines;
}
sub extract_included
{
  my $s = shift;
  my @ret = ();
  while ($s =~ m/(\[.*?]|\w+)/) {
    push @ret, (substr($1,0,1) eq '[' ? trim1($1) : $1);
    $s = "$'";
  }
  push @ret, split /\s+/, trim($s);
  return @ret;
}
sub xtrim
{
  my @a = @_;
  my $n = 10000;
  foreach (@a) {
    my $s = $_->{text};
  	$s =~ m/^(\s*)/;
    next if "$'" eq '';
    $n = length($1) if $n > length($1);
  }
  return @a if $n == 0;

  return map {
    my $s = $_->{text};
    if ($n < length($s)) {
      $s = substr($s, $n);
    } else {
      $s = '';
    }
    $_->{text} = $s;
    $_
  } @a;
}
sub emit {
  my $output = shift;
  my $output_filename = shift;
  my @lines = @_;
  my $loc_format;
  my $lang = ($opt_lang || $g_curr_lang);
  $loc_format = $loc_formats{$lang} if ($lang && $opt_loc);
  my $last_file,$last_ln;

  dbg("emit: ", scalar(@lines), " lines");

  my $ln = 1;
  foreach (@lines) {
    if ($loc_format) {
      if (($last_file ne $_->{file}) || ($last_ln+1 != $_->{ln})) {
        my $x = &$loc_format($_->{file}, $_->{ln});
        print $output $x;
      }
      $last_file = $_->{file};
      $last_ln = $_->{ln};
    } elsif ($output_source_map) {
      print $output_source_map "$output_filename:$ln:$_->{file}:$_->{ln}\n";
      $ln++;
    }
    print $output $_->{text},"\n";
  }
}
use Data::Dumper;

sub docgen_html {
 my $zf = shift;
 my $old = select(shift);
 print "<!DOCTYPE HTML>\n\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex\@0.15.1/dist/katex.min.css\"\n   crossorigin=\"anonymous\">\n <style>\n :root {\n   --navbar-height: 1.6rem;\n }\n \n html,body {\n   margin: 0;\n   padding: 0;\n }\n \n html {\n   font-size: 14pt;\n }\n \n .navbar {\n   background: #ccc;\n   padding: 4px 8px;\n   display: flex;\n   align-items: center;\n   height: var(--navbar-height);\n }\n \n section.folded > .content {\n   display: none;\n }\n section.folded > .heading::before {\n   content: \"+\"\n }\n \n .align-right {\n   text-align: right;\n }\n \n .sticky-top {\n     position: sticky;\n     top: 0;\n }\n \n .container {\n   background: gray;\n }\n \n .xpad {\n   padding: 0 4px;\n }\n .xxpad {\n   padding: 0 8px;\n }\n \n .col-header {\n  display: flex;\n  background: #ccc;\n  height: var(--navbar-height);\n  align-items: center;\n }\n .fill-parent {\n   flex: 1;\n }\n \n p code {\n   background: #eee;\n }\n \n table {\n   border-collapse: collapse;\n   margin: 1em 0;\n }\n \n table, th, td {\n   border: 1px solid black;\n }\n \n th {\n   border-bottom: 4px solid black;\n }\n \n td {\n   padding: 0 4px;\n }\n \n thead > tr {\n   background: #ccc;\n }\n \n .container\n \n .col {\n  background: #eee;\n  overflow: scroll;\n  flex: 2;\n }\n \n \n .col.wide {\n   flex: 4;\n }\n .col.narrow {\n   flex: 1;\n }\n \n article {\n   max-width: 960px;\n   min-width: 320px;\n   margin: auto;\n   word-break: break-word;\n   padding: 1rem 8px;\n }\n \n aside {\n   background: #eee;\n   font-size: 0.8rem;\n   flex: 1;\n   max-height: 100vh;\n   overflow: auto;\n   position: sticky;\n   top: 0;\n }\n \n aside:not(.is-narrow) section {\n   column-width: 240px;\n   font-size: 0.8rem;\n   column-count: 2;\n }\n \n aside.is-wide section {\n   column-width: 240px;\n   font-size: 0.9rem;\n   column-count: 3;\n }\n \n .fullwidth {\n    width: 100%;\n }\n \n .collapse {\n   display: none;\n }\n \n .text-small {\n   font-size: 0.8rem;\n }\n \n section {\n \tscroll-margin-top: var(--navbar-height);\n }\n \n section.sidebar  {\n   position: fixed;\n   bottom: 0px;\n   background: #ccc;\n   overflow: auto;\n   top: var(--navbar-height);\n   z-index: 1000;\n   width: 320px;\n   right: 0;\n }\n \n .col.folded > article {\n   display: none;\n }\n \n \@media screen and (min-width: 960px) {\n   .container {\n     display: flex;\n     flex-direction: row;\n     position: absolute;\n     width: 100%;\n     height: 100%;\n     overflow: hidden;\n   }\n   .container > :not(:first-child) {\n     margin-left: 4px;\n   }\n \n   .col > .col-header {\n       position: sticky;\n       top: 0;\n   }\n \n   .col.folded > article {\n     visibility: hidden; /* can not use display none, because layout will be lost, scroll position lost */\n     display: block;\n   }\n \n   .col.folded > .col-header  {\n     writing-mode: vertical-rl; /* Vertical writing mode, content flows from top to bottom */\n     transform: rotate(180deg); /* Rotate the text 180 degrees */\n     width: 100%; /* Very important to fill the entire col */\n     height: 100%;\n   }\n \n   .col.folded {\n     overflow: hidden;\n     flex: 0;\n     min-width: 30px;\n  }\n \n   aside > section {\n     display: block !important;\n   }\n   #toggle-index {\n     display: none;\n   }\n }\n \n pre, pre.prettyprint {\n   padding: 0.5rem !important;\n   font-size: 0.9rem;\n   white-space: pre-wrap;\n }\n pre.listing {\n   padding-left: 3rem !important;\n   border: 2px solid black;\n }\n \n .line-number {\n   font-size: 0.7rem;\n   margin-left: -3rem !important;\n   margin-right: 1rem;\n   user-select: none; /*  */\n }\n \n \n .L-1 {\n  margin-left: 16px;\n }\n .L-2 {\n  margin-left: 28px;\n }\n .L-3,.L-4,.L-5,.L-6,.L-7,.L-8 {\n  margin-left: 36px;\n }\n \n \n img {\n   width: 100%;\n }\n \@media screen and (min-width: 480px) {\n    img.tiny { width: 50%; }\n }\n \n \@media screen and (min-width: 720px) {\n    img.tiny { width: 33%; }\n    img.small { width: 66%; }\n }\n \n \@media screen and (min-width: 960px) {\n   img.tiny { width: 25%; }\n   img.small { width: 50%; }\n   img.medium { width: 75%; }\n }\n \@media print {\n   img.tiny { width: 2.5in; }\n   img.small { width: 5in; }\n }\n \n \n figure { text-align: center}\n \n \@media print {\n   * {\n     /* Don't waste color ink */\n     color: black !important;\n   }\n   \@page {\n     size: auto; /* Magic: disable browser generated footer and header */\n     margin: 1em;\n     table {\n       page-break-inside: avoid;\n       page-break-after: avoid;\n     }\n   }\n   aside { display: none; }\n }\n \n </style>\n <script>\n   window.onload = function() {\n    function autoResizeTextArea(textarea) {\n      textarea.style.height = 'auto';\n      textarea.style.height = (textarea.scrollHeight + 2) + 'px';\n    }\n    \n    document.addEventListener('click', async function(event) {\n      let el = event.target;\n      let handler = el.getAttribute('click');\n      if (handler == 'edit') {\n    \n        let editor = document.querySelector(\".editor\");\n        if (editor) {\n          let section = editor.closest('section');\n          section.scrollIntoView({ behavior: 'smooth', block: 'start' });\n          return;\n        }\n        let section = el.closest('section');\n        editor = document.createElement(\"div\");\n        editor.classList.add('editor');\n        section.appendChild(editor);\n    \n        let textarea = document.createElement('textarea');\n        textarea.classList.add('fullwidth');\n        textarea.addEventListener('input', function() {\n           this.style.resize = 'none';\n           autoResizeTextArea(this);\n        });\n        editor.appendChild(textarea);\n        el.classList.add('collapse');\n        let saveButton = document.createElement('button');\n        let cancelButton = document.createElement('button');\n        saveButton.innerText = 'Save';\n        editor.appendChild(saveButton);\n        cancelButton.innerText = 'Cancel';\n        editor.appendChild(cancelButton);\n    \n        let l = section.getAttribute('l');\n        let n = section.getAttribute('n');\n        let response = await fetch(`?raw=1&l=\${l}&n=\${n}`);\n        if (!response.ok) {\n          throw new Error(\"Bad\");\n        }\n        textarea.value = await response.text();\n        autoResizeTextArea(textarea);\n        saveButton.onclick = async function() {\n           let res = await fetch(`?l=\${l}&n=\${n}&last_modified=\${window.zen.last_modified}`, {\n             method: 'PUT',\n             headers: {\n               'Content-type': 'text/plain; charset=utf-8',\n             },\n             body: textarea.value\n           });\n           if (!res.ok) {\n             alert(res.status);\n             throw new Error(\"can not save\");\n           }\n           window.location.hash = section.getAttribute('id');\n           window.location.reload();\n        };\n        cancelButton.onclick = function() {\n           el.classList.remove('collapse');\n           section.removeChild(editor);\n        };\n    \n      } else if (handler == 'fold') {\n        let section = el.closest('.foldable');\n        section.classList.toggle('folded');\n      } else if (handler == 'expand') {\n        const col = el.closest('.col');\n        if (col.classList.contains('narrow')) {\n          col.classList.remove('narrow');\n          el.innerText = '+';\n        } else if (col.classList.contains('wide')) {\n          col.classList.remove('wide');\n          el.innerText = '-';\n        } else if (el.innerText == '-') {\n          col.classList.add('narrow');\n          el.innerText = '++';\n        } else if (el.innerText == '+') {\n          col.classList.add('wide');\n          el.innerText = '--';\n        }\n      } else if (handler == 'show-outline') {\n        let col = el.closest('.col');\n        col.querySelectorAll('section').forEach(section => {\n          section.classList.toggle('folded');\n        });\n      } else {\n        let col = el.closest('.col');\n        if (col.tagName == 'ASIDE' && el.tagName == 'A') {\n          if (!col.closest('.container').classList.contains('is-wide')) {\n            col.classList.toggle('folded');\n          }\n          // Unfold the target\n          let t = document.querySelector(el.getAttribute('href'));\n          if (t) {\n            let tcol = t.closest('.col');\n            if (tcol) tcol.classList.remove('folded');\n          }\n        }\n      }\n    });\n    \n    const observer = new ResizeObserver(entries => {\n      for (let entry of entries) {\n        const target = entry.target;\n    \n        if (target.classList.contains('folded'))\n          return;\n    \n        const width = target.getBoundingClientRect().width;\n        if (width < 640) {\n          target.classList.add('is-narrow');\n        } else {\n          target.classList.remove('is-narrow');\n        }\n        if (width > 960) {\n          target.classList.add('is-wide');\n        } else {\n          target.classList.remove('is-wide');\n        }\n      }\n    });\n    \n    document.querySelectorAll('.col,.container').forEach(col => {\n      observer.observe(col);\n    });\n   }\n </script>\n</head>\n<body>";
 print "<script>";
 print "window.zen = ";
 print JSON::encode_json({
   last_modified => $zf->{last_modified},
   size => $zf->{size},
 });
 print "</script>";
 print '<div class="container">';

 print '<aside class="col foldable narrow">';
 print '<div class="col-header sticky-top">';
 print '<span class="fill-parent">';
 if ($opt_live) {
   print '<a href="/">home</a>';
   print '<a href="..">up</a>';
 }
 print '</span>';
 print "<button click=\"expand\">++</button>";
 print "<button click=\"fold\">fold</button>";

 print '</div>';
 print '<article><section>';
 foreach (@sections) {
     my $section = $_;
     next if $section->{hidden};
     my $level = $section->{level};
     my $id = $section->{id};
     print "<a class=\"L-$level\" href=\"#$id\">$section->{heading}</a><br>\n";
 }
 print '</section></article>';
 print '</aside>';

 my @cols = ([]);
 foreach my $section (@sections) {
   next if $section->{hidden};
   if (exists $section->{fields}->{break}) {
     push @cols, [];
   }
   push @{$cols[-1]}, $section;
 }

 foreach my $col (@cols) {
   next if scalar @{$col} == 0;
   print "<div class=\"col foldable\">";
   print "<div class=\"col-header\">";
   print '<span class="fill-parent">', scalar @{$col}, ' sections</span>';
   print "<button click=\"show-outline\">outline</button>";
   print "<button click=\"expand\">+</button>";
   print "<button click=\"fold\">fold</button>";

   print "</div>";
   print "<article>";
   foreach my $section (@{$col}) {
     gen_html_section($section);
   }
   print "</article></div>\n";
 }

 print '</div>';
 print " <script defer src=\"https://cdn.jsdelivr.net/npm/katex\@0.15.1/dist/katex.min.js\" crossorigin=\"anonymous\"></script>\n <script defer src=\"https://cdn.jsdelivr.net/npm/katex\@0.15.1/dist/contrib/auto-render.min.js\"\n   crossorigin=\"anonymous\"\n   onload=\"renderMathInElement(document.body);\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/google/code-prettify\@master/loader/run_prettify.js\"></script>\n </body>\n</html>";
 select($old);
}

sub docgen_line
{
 my $l = shift;
 my @a = parse_doc_line($l);
 foreach (@a) {
  my $tok = $_;
  if ($tok->{type} eq 'plain') {
    print html_escape($tok->{text});
  } elsif ($tok->{type} eq 'emph') {
    print "<em>",html_escape($tok->{text}),"</em>";
  } elsif ($tok->{type} eq 'code') {
    print "<code>",html_escape_code($tok->{text}),"</code>";
  } elsif ($tok->{type} eq 'math') {
    print "\\(",html_escape_code($tok->{text}),"\\)";
  } elsif ($tok->{type} eq 'image') {
    print "<figure>",
      '<img class="', $tok->{size},'" src="',$tok->{url},'">',
      "<figcaption><b>Fig.$tok->{label}</b> ",
        html_escape($tok->{caption}),
      "</figcaption>",
      "<a name=\"Fig-$tok->{label}\"></a>",
      "</figure>\n";
  } elsif ($tok->{type} eq 'ref') {
    if ($tok->{url}) {
      print "<a href=\"$tok->{url}\">",
        html_escape($tok->{text}), "</a>";
    } else {
      if ($tok->{subtype} eq 'sec' || $tok->{subtype} eq 'section') {
        my $sec = find_section($tok->{text});
        print "<a href=\"\#$sec->{id}\">",html_escape($sec->{heading}),"</a>";
      } elsif ($tok->{subtype} eq 'fig') {
        print "<a href=\"\#Fig-$tok->{text}\">$tok->{text}</a>";
      } elsif ($tok->{subtype} eq 'ln') {
        my ($label,$t) = split /:/,$tok->{text};
        my $sec = $l->{section};
        $sec = find_section($t) if $t;
        my $lab = find_label($label, $sec);
        print "<a href=\"\#$lab->{id}\">$lab->{line}</a>";
      } else {
        my ($label,$t) = split /:/,$tok->{text};
        my $lab = find_label($label);
        print "<a href=\"\#$lab->{id}\">$t</a>";
      }
    }
  } elsif ($tok->{type} eq 'link') {
    print "<a href=\"",$tok->{url},"\">", $tok->{url}, "</a>";
  } else {
    die "HTML line: unsupported type: $tok->{type}";
  }
 }
 print "\n";
}
sub html_escape
{
  my ($s) = @_;
  $s =~ s/\&/\&amp;/g;
  $s =~ s/\</&lt;/g;
  $s =~ s/\>/&gt;/g;
  $s =~ s/``/&ldquo;/g;
  $s =~ s/\"/&rdquo;/g;
  return $s;
}
sub html_escape_code
{
  my ($s) = @_;
  $s =~ s/\&/\&amp;/g;
  $s =~ s/\</&lt;/g;
  $s =~ s/\>/&gt;/g;
  return $s;
}
sub gen_html_section {
  my $section = shift;
  next if $section->{hidden};
  my $numLines = scalar @{$section->{lines}};
  my $ln = $numLines > 0 ? $section->{lines}[0]->{ln} : 0;

  print '<section class="foldable" l="', $ln, '" n="', $numLines, '" id="', $section->{id}, '">';
  {
    my $level = $section->{level};
    my $id = $section->{id};
    print "<h$level class=\"heading\" click=\"fold\">",
      $section->{heading},
      ($section->{pattern} ?
       ' <span class="text-small"><code>['
       .   html_escape($section->{pattern})
       . ']</code><i>'
       .   join(',', @{$section->{params}})
       . '</i></span>'
       : ''),
      "</h$level>\n";
  }

  print '<div class="content">';
  my @lines = @{$section->{lines}};
  shift @lines;
  while (@lines > 0) {
    my $l = shift @lines;
    my $t = $l->{text};

    if (exists $l->{begin_block}) {
      if ($l->{begin_block}->{type} eq 'table') {
        my $blk = $l->{begin_block};
        print "<table>\n";
        my @rows = @{$blk->{rows}};
        if ($blk->{has_headers}) {
          my $headers = shift @rows;
          print "<tr>";
          for (@{$headers}) {
            print "<th>$_</th>";
          }
          print "</tr>";
        }
        
        for (@rows) {
          my $a = shift @rows;
          print "<tr>";
          for (@{$a}) {
            print "<td>$_</td>";
          }
          print "</tr>\n";
        }
        
        print "</table>\n";
        while (@lines > 0) {
          $l = shift @lines;
          last if $l->{end_block};
        }
        
      } else {
        my $lang = $l->{begin_block}->{lang};
        
        
        if ($l->{begin_block}->{name}) {
          my $name = $l->{begin_block}->{name};
          if ($name =~ m/^\+\[?([^]]*)/) {
            my $sec = find_section($1);
            my $caption = $sec->{heading};
            print "[<b>",html_escape($caption),"</b>]+&equiv;";
          } elsif ($name =~ m/^>(.*)/) {
            print "&lang;<b>",html_escape($1),"</b>&rang;&equiv;";
          } else {
            print "{<b>",html_escape($name),"</b>}&equiv;";
          }
        }
        
        my $pp;
        $lang = "language-$lang" if $lang;
        $pp = "prettyprint" if $lang;
        my $linenums;
        if ($l->{begin_block}->{first_number} == 1) {
          $linenums = 'first';
        }
        print "<pre class=\"listing $pp\"><code class=\"$lang\">";
        while (@lines > 0) {
          $l = shift @lines;
          last if $l->{end_block};
          my $label;
          $t = $l->{text};
          if ($t =~ m/$re_ln_label/) {
            $label = $1;
            $t = $`;
          }
        
          if ($l->{id}) { # numbered line
           print sprintf('<span class="nocode line-number">%3d</span>', $l->{id});
          }
        
          if ($t =~ m/^(\s*)\[\[(.*)]]\s*$/) {
             my $spaces = $1;
             my @names = split /;/,$2;
             print $spaces,"<span class=\"nocode\">&#x27e6;</span>";
             my $n = 0;
             foreach (@names) {
               $n++;
               my ($name, $params) = split /:/,trim($_);
               my $section = find_section($name);
               my $id = $section->{id};
               if ($params) {
                 $params = "<small>(" . html_escape($params) . ")</small>";
               }
               print "; " if $n > 1;
               $name = $section->{heading} if index($name, '...') != -1;
               print "<span class=\"nocode\"><a href=\"#$id\" title=\"$section->{heading}\"><b>$name</b></a>$params</span>";
             }
             print "<span class=\"nocode\">&#x27e7;</span>";
          } elsif ($t =~ m/^(\s*)\{\{(.*)}}\s*$/) {
             my $spaces = $1;
             my $names = $2;
        
             print $spaces,"<span class=\"nocode\">&#x2983;<b>",
               html_escape($names),  "</b>&#x2984;</span>";
        
          } elsif($t =~ m/^(\s*)\.label\s+(.*)/) {
            my $name = $2;
            my $lab = find_label($name, $section);
            print "<a name=\"$lab->{id}\"></a>";
          } elsif($t =~ m/^(\s*)`([`.[].*)/) {
            my $s = html_escape_code($1 . $2);
            $s = ' ' unless $s; # Force to show a line in pretty
            print "$s";
          } else {
            my $s = html_escape_code($t);
            $s = ' ' unless $s; # Force to show a line in pretty
            print "$s";
          }
        
          if ($label) {
            my $lab = find_label($label, $section);
            print "<a name=\"$lab->{id}\"></a>";
          }
          print "\n";
        }
        print "</code></pre>\n";
      }
    } elsif ($t =~ m/^(```+)(\w+)?\s+%(.*)/) {
      
      my $backticks = $1;
      my $lang = $2;
      my $cmd = $3;
      my @a = ();
      while (@lines > 0) {
        my $l = shift @lines;
        last if ($l->{text} =~ m/^$backticks\s*$/);
        push @a, expand1($l);
      }
      my $src = join("\n", map {$_->{text}} xtrim(@a));
      my $input_file, $output_file;
      my $dir = 'tmp';
      if ( !-d  $dir) {
          make_path $dir or die "Failed to create path: $dir";
      }
      
      if ($cmd) {
      } else {
        if ($lang eq "plantuml") {
          $input_file = $dir . '/' . sha256_hex($src) . ".puml";
          $output_file = $dir . '/' . sha256_hex($src) . ".svg";
          $cmd = "plantuml -tsvg $input_file";
        } elsif ($lang eq "gnuplot") {
          $input_file = $dir . '/' . sha256_hex($src) . ".gnuplot";
          $output_file = $dir . '/' . sha256_hex($src) . ".svg";
          $src = "set term svg\nset output \"$output_file\"\n$src";
          $cmd = "gnuplot -c $input_file";
        }
      }
      
      if ($input_file && $cmd) {
        open(my $fh, '>', $input_file) or die "Could not open file '$input_file' $!";
        print $fh $src;
        close $fh;
        dbg("running: $cmd");
        system($cmd);
      }
      
      if ($output_file =~ m/\.(svg|png)$/) {
        print "<img src=\"$output_file\">";
      } else {
        print "<xmp>$src</xmp>";
      }
      
    } elsif ($t =~ m/^(```+)(\w+)?/) {
      my $backticks = $1;
      my $lang = $2;
      my $prettyprint = "prettyprint" if $lang;
      $lang = "language-$lang" if $lang;
      print "<pre class=\"$prettyprint\"><code class=\"$lang\">";
      my @a = ();
      while (@lines > 0) {
        my $l = shift @lines;
        last if ($l->{text} =~ m/^$backticks\s*$/);
        push @a, $l;
      }
      foreach (xtrim(@a)) {
        print html_escape_code($_->{text}), "\n";
      }
      print "</code></pre>\n";
    } elsif ($t =~ m/^\s*\.label\s+(\w+)/) {
      my $lab = find_label($1, $section);
      print "<a name=\"$lab->{id}\"></a>";
    } elsif ($t =~ m/^\.def\s+(\w+)\s+(.*)/) {
      print "{<b>$1</b>} &equiv; <code>",html_escape_code($2),"</code><br>\n";
    } elsif ($t =~ m/$re_defvar/) {
      print "<div>{<b>$1</b>} &equiv; <code>",html_escape_code($2),"</code></div>\n";
    } elsif ($t =~ m/^\.(\w+)\s+(.*)/) {
      print "<code><b>.$1</b></code> <code>",html_escape_code($2),"</code>\n";
    } elsif ($t =~ m/^\$\$/) {
      print "\\[\n";
      while (@lines > 0) {
        my $l = shift @lines;
        last if $l->{text} =~ m/^\$\$/;
        print html_escape_code($l->{text}), "\n";
      }
      print "\\]\n";
    } elsif ($t =~ m/^(\s*)-\s+(.*)/) {
      my $indent = length($1);
      print "<ul>\n";
      print "<li> ";
      docgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$2});
      
      while (@lines > 0) {
        my $l = shift @lines;
        if ($l->{text} =~ m/^\s{$indent}-\s+(.*)/) {
          print "<li> ";
          docgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
        } elsif ($l->{text} =~ m/^\s{$indent}\s+(.*)/) {
          docgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
        } else {
          unshift @lines, $l;
          last;
        }
      }
      print "</ul>\n";
    } elsif ($t =~ m/^(\s*)\d+\.\s+(.*)/) {
      my $indent = length($1);
      print "<ol>";
      print "<li> ";
      docgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$2});
      
      while (@lines > 0) {
        my $l = shift @lines;
        if ($l->{text} =~ m/^\s{$indent}\d+\.\s+(.*)/) {
          print "<li> ";
          docgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
        } elsif ($l->{text} =~ m/^\s{$indent}\s+(.*)/) {
          docgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
        } else {
          unshift @lines, $l;
          last;
        }
      }
      print "</ol>\n";
    } elsif ($t =~ m/^\s*$/) {
      while (@lines > 0) {
        $l = shift @lines;
        if ($l->{text} =~ m/^\s*$/) {
          next;
        } else {
          unshift @lines, $l;
          last;
        }
      }
      print "<p>";
    } elsif ($t =~ m/^>/) {
      print "<blockquote>";
      print html_escape(substr($t,1)), "\n";
      while (@lines > 0) {
        $l = shift @lines;
        $t = $l->{text};
        if ($t =~ m/^>/) {
          print html_escape(substr($t,1)), "\n";
        } else {
          unshift @lines, $l;
          last;
        }
      }
      print "</blockquote>";
    } else {
      docgen_line($l);
    }
  }
  {
    my @inc = ();
    my @ext = ();
    foreach (@{$section->{refs}}) {
      my $ref = $_;
      #print STDERR "REF! $ref->{type} $ref->{section}->{heading}\n";
      push @ext, $ref->{section} if ($ref->{type} eq 'extended');
      push @inc, $ref->{section} if ($ref->{type} eq 'included');
    }
    my $ninc = scalar(@inc);
    my $next = scalar(@ext);
    if ($next > 0) {
      print "<p class=\"text-small\">This section is extended by ";
      for (my $i = 0; $i < $next; $i++) {
        my $sec = $ext[$i];
        print "<a href=\"\#$sec->{id}\">",html_escape($sec->{heading}),"</a>";
        print $i == $ninc - 1 ? '. ' : '; ';
      }
      print "</p>\n";
    }
  
    if ($ninc > 0) {
      print "<p class=\"text-small\">This section is used in ";
      for (my $i = 0; $i < $ninc; $i++) {
        my $sec = $inc[$i];
        print "<a href=\"\#$sec->{id}\">",html_escape($sec->{heading}),"</a>";
        print $i == $ninc - 1 ? '. ' : '; ';
      }
      print "</p>\n";
    }
  
  }
  if ($opt_live) {
    my $lang;
    foreach (@{$section->{blocks}}) {
      $lang = $_->{lang};
      last if $lang;
    }
    print "<div class=\"text-small\">";
    if ($lang) {
      print "<a href=\"?section=$section->{heading}\">Show output</a> ";
  
      if ($lang eq 'bash' || $lang eq 'sh' || $lang eq 'pl' || $lang eq 'perl'
       || $lang eq 'ruby')
      {
        print "<a href=\"?action=exec&section=$section->{heading}\">Execute</a>";
      }
    }
    print '<button click="edit">Edit</button> ';
    print "</div>\n";
  }
  print "</div>";
  print "</section>\n";
}

sub docgen_latex {
 my $old = select(shift);
 my $opts;
 $opts = 'draft' if $opt_draft;
 print "\\documentclass[$opts]{$opt_document_class}\n",
 "\\usepackage[paperwidth=6in,paperheight=9in,margin={.5in,.5in}]{geometry}",
 "\\usepackage{hyperref}\n",
 "\\usepackage{listings}\n",
 "\\usepackage{graphicx}\n",
 "\\usepackage{upquote}\n",
 "\\usepackage{lmodern}\n",
 "\\begin{document}\n",
 $opt_toc ? "\\tableofcontents\n" : '',
 "\\lstset{\n",
 "numbers=left, numberstyle=\\tiny, stepnumber=1,\nnumbersep=8pt, firstnumber=1,upquote=true,\nframe=single, breaklines=true,\npostbreak=\\mbox{\\small{\$\\hookrightarrow\$}\\space},\nbasicstyle=\\ttfamily\\small,\n",
 "  escapeinside={/*","!}{!*/}",
 "}\n";
 foreach (@sections) {
   my $section = $_;
   my $num_listings = 0; 
 
   {
     my $level = $section->{level};
     my @params = @{$section->{params}};
     my $level_name = $section_levels[$level-1];
     my $id = $section->{id};
   
     my $short_title = latex_escape($section->{heading})
       . ($section->{max_line_id} > 1 ?
           "{\\tiny \[".($section->{max_line_id} - 1)."]}"
         : '');
   
     print "\\$level_name", "[$short_title]{",
        latex_escape($section->{heading}),
        ($section->{pattern} ?
          ' \\hfill{\\footnotesize \\texttt{['
          .   latex_escape($section->{pattern})
          . ']}~\\emph{'
          .   join(',', @params)
          . '}}'
          : ''),
        "} \\label{$id}\n";
   }
 
   my @lines = @{$section->{lines}};
   shift @lines;
   while (@lines > 0) {
     my $l = shift @lines;
     my $t = $l->{text};
 
     if ($l->{begin_block}) {
       if ($l->{begin_block}->{type} eq 'table') {
         my $blk = $l->{begin_block};
         my @rows = @{$blk->{rows}};
         print "\\begin{tabular}";
         print "{|", join("|", ('c') x scalar(@{$rows[0]})), "|}\n\\hline\n";
         if ($blk->{has_headers}) {
           my $headers = shift @rows;
           print join("&",@{$headers});
           print " \\\\\n\\hline\n\\hline\n";
         }
         
         for (@rows) {
           my $a = shift @rows;
           print join("&",@{$a});
           print " \\\\\n\\hline\n";
         }
         
         print "\\end{tabular}\n";
         while (@lines > 0) {
           $l = shift @lines;
           last if $l->{end_block};
         }
         
       } else {
         my $firstnum = ($num_listings == 0 ? 1 : 'last');
         my $caption = $l->{begin_block}->{name};
         my $lang = $l->{begin_block}->{lang};
         $lang = undef unless exists $latex_listing_supported_languages{$lang};
         
         if ($l->{begin_block}->{name}) {
           my $name = $l->{begin_block}->{name};
           print "\\vskip 0.5em \\noindent ";
           if ($name =~ m/^\+\[?([^]]*)/) {
             my $sec = find_section($1);
             my $caption = $sec->{heading};
             print '$[\\![$\\textbf{', latex_escape($caption), '}$]\\!] +\\!\\!\\equiv$';
           } elsif ($name =~ m/^>(.*)/) {
             print '$\\langle$\\textbf{',latex_escape($1), '}$\\rangle\\equiv$';
           } else {
             print "\\{\\textbf{",latex_escape($name),'}\\}$\\equiv$';
           }
         }
         
         my $options = "firstnumber=$firstnum";
         $options .= ",language=$lang" if $lang;
         print "\\begin{lstlisting}[$options]\n";
         
         $num_listings++;
         
         while (@lines > 0) {
           $l = shift @lines;
           $t = $l->{text};
           my $label;
         
           last if $l->{end_block};
         
           if ($t =~ m/$re_ln_label/) {
             $t = $`;
             $label = $1;
           }
         
           if ($t =~ m/^(\s*)\[\[(.*)]]\s*$/) {
             my $spaces = $1;
             my @names = split /;/, $2;
             print $spaces, "/*", '!$[\\![$';
             my $n = 0;
             foreach (@names) {
               $n++;
               my ($name, $params) = split /:/,trim($_);
               my $section = find_section($name);
               my $id = $section->{id};
         	  if ($section->{pattern} && !$params && @{$section->{params}} > 0) {
                 my $pat = $section->{pattern};
                 my @vals = ($name =~ /^$pat$/g);
         	    $params = join(',', @vals);
         	  }
               if ($params) {
                 $params = "{(" . latex_escape($params) . ")}";
               }
               print "; " if $n > 1;
               if (scalar(@names)==1) {
                 print "\\textrm{{",latex_escape($section->{heading}),"}}$params",
                   " {\\scriptsize \\pageref{$id}}";
               } else {
                 $name = latex_escape($name);
                 print "\\textrm{{$name}}$params {\\scriptsize \\pageref{$id}}";
               }
             }
             print '$]\\!]$', "!*/";
           } elsif($t =~ m/^(\s*)\.label\s+(.*)/) {
             my $name = $2;
             my $lab = find_label($name, $section);
             print '/*',"!\\label{$lab->{id}}!*/";
           } elsif($t =~ m/^(\s*)`([`.[].*)/) {
             print $1,$2;
           } else {
             print $t;
           }
         
           if ($label) {
             my $lab = find_label($label, $section);
             print '/*',"!\\label{$lab->{id}}!*/";
           }
           print "\n";
         }
         print "\\end","{lstlisting}\n";
       }
     } elsif ($t =~ m/^(```+)(\w+)?\s+%(.*)/) {
       my $backticks = $1;
       my $lang = $2;
       my $cmd = $3;
       my @a = ();
       while (@lines > 0) {
         my $l = shift @lines;
         last if ($l->{text} =~ m/^$backticks\s*$/);
         push @a, expand1($l);
       }
       my $src = join("\n", map {$_->{text}} xtrim(@a));
       my $input_file, $output_file;
       my $dir = 'tmp';
       if ( !-d  $dir) {
           make_path $dir or die "Failed to create path: $dir";
       }
       
       if ($cmd) {
       } else {
         if ($lang eq "plantuml") {
           $input_file = $dir . '/' . sha256_hex($src) . ".puml";
           $output_file = $dir . '/' . sha256_hex($src) . ".pdf";
           $cmd = "plantuml -tpdf $input_file";
         } elsif ($lang eq "gnuplot") {
           $input_file = $dir . '/' . sha256_hex($src) . ".gnuplot";
           $output_file = $dir . '/' . sha256_hex($src) . ".pdf";
           $src = "set term pdf\nset output \"$output_file\"\n$src";
           $cmd = "gnuplot -c $input_file";
         }
       }
       
       if ($input_file && $cmd) {
         open(my $fh, '>', $input_file) or die "Could not open file '$input_file' $!";
         print $fh $src;
         close $fh;
         dbg("running: $cmd");
         system($cmd);
       }
       
       if ($output_file =~ m/\.(pdf)$/) {
         print "\\includegraphics[width=\\textwidth]{$output_file}";
       } else {
         print "$src";
       }
       
     } elsif ($t =~ m/^(```+)(\w+)?/) {
       my $backticks = $1;
       my $lang = $2;
       my $options="frame=none,numbers=none";
       
       $options .= ",language=$lang" if $lang && exists $latex_listing_supported_languages{$lang};
       
       print "\\begin{lstlisting}[$options]\n";
       my @a = ();
       while (@lines > 0) {
         my $l = shift @lines;
         last if ($l->{text} =~ m/^$backticks\s*$/);
         push @a, $l;
       }
       foreach (xtrim(@a)) {
         print $_->{text}, "\n";
       }
       print "\\end","{lstlisting}\n";
     } elsif ($t =~ m/^\s*\.label\s+(.*)/) {
       my $lab = find_label($1);
       print "\\label{$lab->{id}}\n";
     } elsif ($t =~ m/^\s*\{#(\w+)}/) {
       my $lab = find_label($1);
       print "\\label{$lab->{id}}\n";
     } elsif ($t =~ m/^\.def\s+(\w+)\s+(.*)/) {
       print "\\noindent", '$\\langle$', "\\verb|$1|", '$\\rangle$',
         "\$\\equiv\$ \\verb|$2|\\newline\n";
     } elsif ($t =~ m/$re_defvar/) {
       print "\\noindent", '$\\{$', "\\verb|$1|", '$\\}$',
         "\$\\equiv\$ \\verb|$2|\\newline\n";
     } elsif ($t =~ m/^\.(\w+)\s+(.*)/) {
       print "\\textbf{.$1} \\verb|$2|\n";
     } elsif ($t =~ m/^\$\$/) {
       print "\\[\n";
       while (@lines > 0) {
         my $l = shift @lines;
         last if $l->{text} =~ m/^\$\$/;
         print $l->{text}, "\n";
       }
       print "\\]\n";
     } elsif ($t =~ m/^(\s*)-\s+(.*)/) {
       my $indent = length($1);
       print "\\begin{itemize}\n";
       print "\\item ";
       latexgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$2});
       
       while (@lines > 0) {
         my $l = shift @lines;
         if ($l->{text} =~ m/^\s{$indent}-\s+(.*)/) {
           print "\\item ";
           latexgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
         } elsif ($l->{text} =~ m/^\s{$indent}\s+(.*)/) {
           latexgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
         } else {
           unshift @lines, $l;
           last;
         }
       }
       print "\\end{itemize}\n";
     } elsif ($t =~ m/^(\s*)\d+\.\s+(.*)/) {
       my $indent = length($1);
       print "\\begin{enumerate}\n";
       print "\\item ";
       latexgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$2});
       
       while (@lines > 0) {
         my $l = shift @lines;
         if ($l->{text} =~ m/^\s{$indent}\d+\.\s+(.*)/) {
           print "\\item ";
           latexgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
         } elsif ($l->{text} =~ m/^\s{$indent}\s+(.*)/) {
           latexgen_line({file=>$l->{file}, ln=>$l->{ln}, text=>$1});
         } else {
           unshift @lines, $l;
           last;
         }
       }
       print "\\end{enumerate}\n";
     } elsif ($t =~ m/^>/) {
       print "\\begin{quote}";
       latexgen_line(dupln($l, substr($t,1)));
       while (@lines > 0) {
         $l = shift @lines;
         $t = $l->{text};
         if ($t =~ m/^>/) {
           latexgen_line(dupln($l, substr($t,1)));
         } else {
           unshift @lines, $l;
           last;
         }
       }
       print "\\end{quote}";
     } else {
       latexgen_line($l);
     }
   }
 
   {
     my @inc = ();
     my @ext = ();
     foreach (@{$section->{refs}}) {
       my $ref = $_;
       push @ext, $ref->{section} if ($ref->{type} eq 'extended');
       push @inc, $ref->{section} if ($ref->{type} eq 'included');
     }
     my $ninc = scalar(@inc);
     my $next = scalar(@ext);
   
     if ($next > 0) {
       print "\n\n{\\footnotesize This section is extended by \n";
       for (my $i = 0; $i < $next; $i++) {
         my $sec = $ext[$i];
         print "\\textit{$sec->{heading}}",
          "{  \\tiny p.~\\pageref{$sec->{id}}}",
          $i == $next - 1 ? '. ' : '; ';
       }
       print "}\n";
     }
   
     if ($ninc > 0) {
       print "\n\n{\\footnotesize This section is used in \n";
       for (my $i = 0; $i < $ninc; $i++) {
         my $sec = $inc[$i];
         print "\\textit{$sec->{heading}}",
          "{  \\tiny \\pageref{$sec->{id}}}",
          $i == $ninc - 1 ? '. ' : '; ';
       }
       print "}\n";
     }
   }
 
 }
 

 print '\end{document}';
 select($old);
}


sub latexgen_line
{
 my $l = shift;
 my @a = parse_doc_line($l);
 foreach (@a) {
  my $tok = $_;
  if ($tok->{type} eq 'plain') {
    print latex_escape($tok->{text});
  } elsif ($tok->{type} eq 'emph') {
    print "\\emph{",latex_escape($tok->{text}),"}";
  } elsif ($tok->{type} eq 'code') {
    if (index($tok->{text}, '|') == -1) {
      print "\\verb|",$tok->{text},"|";
    } elsif (index($tok->{text}, '$') == -1) {
      print "\\verb\$",$tok->{text},"\$";
    } else {
      print "\\verb!",$tok->{text},"!";
    }
  } elsif ($tok->{type} eq 'math') {
    print "\\(",$tok->{text},"\\)";
  } elsif ($tok->{type} eq 'image') {
    my $width = "\\linewidth";
    $width = "0.3" . $width if $tok->{size} eq 'tiny';
    $width = "0.5" . $width if $tok->{size} eq 'small';
    $width = "0.7" . $width if $tok->{size} eq 'medium';

    print "\\begin{figure}\n",
      "\\centering\n",
      "\\includegraphics[width=$width]{$tok->{url}}\n",
      "\\caption{$tok->{caption}}\n",
      "\\label{Fig-$tok->{label}}\n",
      "\\end{figure}\n";
  } elsif ($tok->{type} eq 'ref') {
    if ($tok->{url}) {
      print "\\href{$tok->{url}}{$tok->{text}}";
    } else {
      if ($tok->{subtype} eq 'sec') {
        my $sec = find_section($tok->{text});
        print "\\ref{$sec->{id}}";
      } elsif ($tok->{subtype} eq 'section') {
        my $sec = find_section($tok->{text});
        print "\\emph{$sec->{heading}}",
          "{  \\tiny p.~\\pageref{$sec->{id}}}";
      } elsif ($tok->{subtype} eq 'fig') {
        print "\\ref{Fig-$tok->{text}}";
      } elsif ($tok->{subtype} eq 'ln') {
        my ($label,$t) = split /:/,$tok->{text};
        my $sec = $l->{section};
        $sec = find_section($t) if $t;
        my $lab = find_label($label, $sec);
        print "\\ref{$lab->{id}}";
      } else {
        my ($label,$t) = split /:/,$tok->{text};
        my $lab = find_label($label);
        print "\\emph{$t}", "{ \\tiny p.~\\pageref{$lab->{id}}}";
      }
    }
  } elsif ($tok->{type} eq 'link') {
    print "\\url{",$tok->{url},"}";
  } else {
    die "LaTeX line: unsupported type: $tok->{type}";
  }
 }
 print "\n";
}

sub latex_escape
{
   my $s = shift;
   $s =~ s/(\\|[\${}_#&%^])/\\\1/g;
   return $s;
}
sub parse_doc_line
{
  my $l = shift;
  my $s = $l->{text};
  my @ret = ();
  my $re_link = qr/!?\[(.+?)](\(.+?\))?|\w+:\/\/\S+/;
  while ($s =~ m/(\<.+?\>|\*.+?\*|=.+?=|\|.+?\||`.+?`|\$.+?\$|$re_link|\b#\w+\b)/) {
    push @ret, { type=> 'plain', text=> $` } if length($`);
    my $t = $1;
    my $prefix = substr($t, 0, 1);
    if ($prefix eq '*') {
      push @ret, { type=>'emph', text=>trim1($t) };
    } elsif ($prefix eq '$') {
      push @ret, { type=>'math', text=>trim1($t) };
    } elsif ($prefix eq '`' || $prefix eq '|' || $prefix eq '=') {
      push @ret, { type=>'code', text=>trim1($t) };
    } elsif ($prefix eq '!') {
      my ($caption, $label) = split /:/,$2;
      my $url = trim1($3);
      my $size;
      if ($url =~ m/-(small|tiny|medium|large)\./) {
        $size = $1;
      }
      push @ret, { type=>'image', caption=>$caption, label=>$label,
        url=>$url, size=>$size };
    } elsif ($prefix eq '<') {
      push @ret, { type=>'link', url=>trim1($t) };
    } elsif ($prefix eq '[') {
      if ($3) {
        push @ret, { type=>'ref', text=>$2, url=>trim1($3) };
      } else {
        my @a = split /:/,$2;
        if ($2 =~ m/(sec|ln|fig|tbl|ref):(.*)/) {
          push @ret, {type=>'ref', subtype => $1, text=>$2};
        } else {
          push @ret, {type=>'plain', text=>$t};
        }
      }
    } else {
      push @ret, { type=>'link', url=> $t };
    }
   	$s = "$'";
  }

  push @ret, {type=> 'plain', text=>$s}  if length($s);
  return @ret;
}


sub info {
  print @_, "\n" unless $opt_verbose;
}

sub dbg {
  print STDERR "DEBUG: ", @_, "\n" if $opt_debug;
}


sub trim1 {
  my $s = shift;
  return substr($s, 1, length($s)-2);
}

sub ltrim {
  my $s = shift;
  $s =~ s/^\s+//;
  return $s;
};

sub rtrim {
  my $s = shift;
  $s =~ s/\s+$//;
  return $s;
};

sub trim {
  my $s = shift;
  $s =~ s/^\s+|\s+$//g;
  return $s
};


sub has_prefix {
  my ($s,$t)=@_;
  return index($s,$t) == 0;
}

sub indent {
  my $s = shift;
  return map { $_->{text} = $s . $_->{text}; $_ } @_;
}
sub parse_table_row {
  my $s = shift;
  my @a = split(/\|/, $s, -1); # -1 is important otherwise stripped empty
  return map { trim($_) } splice(@a, 1, -1);
}

sub mpart {
  my $s = shift;
  my @a = @_;
  print "Matching: '$s' with ", join('...', @a), "\n";

  return -1                      if scalar(@a) == 0;
  return ($a[0] eq $s) ? () : -1 if scalar(@a) == 1;

  # Test if $s starts with $first.
  my $first = shift(@a);
  return if rindex($s, $first, 0) != 0;

  # Test if $s ends with $last.
  my $last = pop(@a);
  my $l = length($s) - length($last);
  return if $last ne substr($s, $l);

  # Fetch the fillers between parts
  my $pos = length($first);
  my @r = ();
  for (@a) {
    my $i = index($s, $_, $pos);
    return if $i == -1 || $i >= $l;
    push @r, substr($s, $pos, $i - $pos);
    $pos = $i + length($_);
  }
  push @r, substr($s, $pos, $l - $pos);
  return @r;
}

sub generate_matcher {
  my $pattern = shift;

  my $re = qr/(\.\.\.|\{.*?})/;
  my $s = $heading;
  my @parts = split(/$re/, $pattern);
  if (scalar(@parts) == 1) {
    return sub {
      my $t = shift;
      return {} if $t eq $pattern;
    };
  } else {
    my @fixed = ();
    my @names = ();
    for (my $i = 0; $i < scalar(@parts); $i++) {
      if ($i % 2 == 0) {
        push @fixed, $parts[$i];
      } elsif ($parts[$i] eq '...') {
        push @names, $parts[$i];
      } else {
        push @names, trim1($parts[$i]);
      }
    }
    if (scalar(@fixed) == scalar(@names)) {
      push @fixed, '';
    }
    return sub {
      my $t = shift;
      my @vals = mpart($t, @fixed);
      return unless @vals;
      my $ret = {};
      for (my $i = 0; $i < scalar(@names); $i++) {
        my $name = $names[$i];
        if ($name ne '...') {
           $ret->{$name} = $vals[$i];
        }
      }
      return $ret;
    };
  }
}

sub err
{
  my $msg = shift;
  my $i = 0;
  print STDERR "! Error: $msg\n";
  foreach (@{$env_chain}) {
    print STDERR "  $i: ",$_->{__loc},"\n";
    $i++;
  }
  exit(1);
}


if ($opt_daemon) {
use warnings;
use strict;

{
package App::zen::WebServer;

use HTTP::Server::Simple::CGI;
use base qw(HTTP::Server::Simple::CGI);
use File::Slurp;
use File::Type;
use File::stat;
use JSON;
use Fcntl qw(:flock);
use POSIX qw( strftime );
use Data::Dumper;

my $ft = File::Type->new();

sub handle_request {
    my ($self, $cgi) = @_;

    my $path = $cgi->path_info();
    my $method = $cgi->request_method();

    if ($method eq 'GET' && $path =~ m/\/$/) {
      on_list_dir($cgi, ".$path");
    } elsif ($method eq 'GET' && $path eq '/hello') {
      on_hello($cgi);
    } elsif ($method eq 'GET') {
      on_get_file($cgi, ".$path");
    } elsif ($method eq 'POST') {
      on_append_file($cgi, ".$path");
    } elsif ($method eq 'PUT') {
      on_put_file($cgi, ".$path");
    } else {
      print "HTTP/1.0 400 Bad Request\r\n\r\nUnsupported Request\n";
    }
}

 # Should return  the data since last fetch offset.
sub on_append_file {
  my ($cgi, $path) = @_;
  my $data = $cgi->param('POSTDATA');
  append_to_file($path, $data);
  my @stat_info = stat($path);
  if (@stat_info) {
    # Extract the last modification time
    my $mtime = $stat_info[9];
    print "HTTP/1.0 200 OK\r\n";
    print "Last-Modified: $mtime\r\n";
    print "Content-Length: 0\r\n\r\n";
  } else {
    print "HTTP/1.0 500 Server error\r\n";
  }
}

sub write_error {
  my ($code, $message, $body) = @_;
  print "HTTP/1.1 $code $message\r\n\r\n";
  print $body, "\n";
}

sub on_put_file {
  my ($cgi, $path) = @_;

  my $l = $cgi->url_param('l') || 0;
  my $n = $cgi->url_param('n') || 0;
  my $last_mtime = $cgi->url_param('last_modified');

  if (-f $path) {
    # override require last_mtime
    my $stat = stat($path);
    my $mtime = $stat->mtime;
    if (!$last_mtime) {
      write_error(409, "Conflict", "Missing query parameter 'last_modified' l=$l");
      print Dumper $cgi;
      return;
    }
    if ($last_mtime != $mtime) {
      write_error(409, "Conflict", "last modified doesn't match: $mtime");
      return;
    }
  }
  my $data = $cgi->param('PUTDATA');
  if ($l == 0) {
    write_file($path, $data);
  } elsif ($l > 0) {
    $data .= "\n" unless !$data || substr($data, -1) eq "\n";
    open my $fh, '<', $path or die "Can not open file: $!";
    my $x = '';
    my $y = '';
    while (<$fh>) {
      if ($l > 1) {
        $x .= $_;
        $l--;
      } elsif ($l == 1) {
        if ($n <= 0) {
          $y .= $_;
        } else {
          $n--;
        }
      }
    }
    close $fh;
    write_file($path, $x.$data.$y);
  } else {
    print "HTTP/1.0 400 Bad request\r\n";
    return;
  }
  my $mtime = stat($path)->mtime;
  if ($mtime) {
    print "HTTP/1.0 200 OK\r\n";
    print "Last-Modified: $mtime\r\n";
    print "Content-Length: 0\r\n\r\n";
  } else {
    print "HTTP/1.0 500 Server error\r\n";
  }
}

sub on_get_file {
  my ($cgi, $path) = @_;

  if (-d $path) {
    print "HTTP/1.1 301 Moved Permanently\r\n";
    print "Location: ", $cgi->path_info(), "/\r\n";
    print "\r\n";
    return;
  }

  unless (-f $path) {
    print "HTTP/1.0 404 Not found\r\n";
    print $cgi->header,
      $cgi->start_html('Not found'),
      $cgi->h1('Not found'),
      $path,
      $cgi->end_html;
    return;
  }

  my $raw = $cgi->param('raw') || 0;
  my $l = $cgi->param('l') || 0;
  my $n = $cgi->param('n') || 0;
  if ($path =~ m/\.(md|zen)$/ && ($raw ne '1')) {
    print "HTTP/1.0 200 OK\r\n";
    my $action = $cgi->param('action');
    my $section = $cgi->param('section');
    $action = 'view' unless $action;
    if ($action eq 'exec') {
       print "Content-Type: text/plain\r\n\r\n";
       open FH, "perl ./zen2.pl --exec \"$section\" $path|";
    } else {
      if (!$section) {
         print "Content-Type: text/html\r\n\r\n";
         open FH, "perl ./zen2.pl --live --html $path|";
      } else {
         print "Content-Type: text/plain\r\n\r\n";
         open FH, "perl ./zen2.pl --section \"$section\" $path|";
      }
    }
    while (<FH>) {
       print $_;
    }
    close FH;
  } elsif ($raw eq '1' && $l > 0) {
    my $mtime = stat($path)->mtime;
    print "HTTP/1.0 200 OK\r\n";
    print "Last-Modified: $mtime\r\n";
    print "Content-Type: text/plain\r\n\r\n";
    open my $fh, '<', $path or die "Can not open file: $!";
    while (<$fh>) {
      if ($l == 1) {
        last if $n == 0;
        print $_;
        $n--;
      } else {
        $l--;
      }
    }
    close $fh;
  } else {
    my $mime_type = $ft->mime_type($path);
    open my $fh, '<:raw', $path or die "Cannot open file: $!";
    my $mtime = stat($path)->mtime;
    my $filesize = -s $path;
    print "HTTP/1.1 200 OK\r\n";
    print "Content-Type: $mime_type\r\n";
    print "Last-Modified: $mtime\r\n";
    print "Content-Length: $filesize\r\n";
    print "\r\n";

    # Print the binary content of the file to the CGI output
    binmode STDOUT;
    while (read $fh, my $buffer, 4096) {
        print $buffer;
    }
    close $fh;
  }
}

sub on_list_dir {
  my ($cgi, $path) = @_;

  if (!-d $path) {
    write_error(404, "Not found");
    return;
  }

  my @contents = ();
  opendir(my $dh, $path) or die "Cannot open directory: $!";
  while (my $file = readdir $dh) {
    next if $file =~ /^\./;
    next if $file =~ /~$/;
    my $filepath = "$path/$file";
    my $mtime = stat($filepath)->mtime;
    push @contents, {
      name => -d $filepath ? "$file/" : $file,
      size => -s "$path/$file",
      mtime => $mtime,
    };
  }
  @contents = sort { $a->{name} cmp $b->{name} } @contents;

  my $fmt = $cgi->param('fmt');
  if ($fmt) {
    if ($fmt eq "csv") {
      print "HTTP/1.1 200 OK\r\n";
      print "\r\n";
      for (@contents) {
        print "$_->{name},$_->{size},$_->{mtime}\n";
      }
      return;
    } elsif ($fmt eq 'json') {
      print "HTTP/1.1 200 OK\r\n";
      print "\r\n";
      print JSON::encode_json(\@contents);
      return;
    } else {
      write_error(400, "Bad parameter: unsupported fmt");
      return;
    }
  }

  print "HTTP/1.0 200 OK\r\n\r\n";
  print "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Index</title>\n    <style>\n      :root {\n        --navbar-height: 1.6rem;\n      }\n      \n      html,body {\n        margin: 0;\n        padding: 0;\n      }\n      \n      html {\n        font-size: 14pt;\n      }\n      \n      .navbar {\n        background: #ccc;\n        padding: 4px 8px;\n        display: flex;\n        align-items: center;\n        height: var(--navbar-height);\n      }\n      \n      section.folded > .content {\n        display: none;\n      }\n      section.folded > .heading::before {\n        content: \"+\"\n      }\n      \n      .align-right {\n        text-align: right;\n      }\n      \n      .sticky-top {\n          position: sticky;\n          top: 0;\n      }\n      \n      .container {\n        background: gray;\n      }\n      \n      .xpad {\n        padding: 0 4px;\n      }\n      .xxpad {\n        padding: 0 8px;\n      }\n      \n      .col-header {\n       display: flex;\n       background: #ccc;\n       height: var(--navbar-height);\n       align-items: center;\n      }\n      .fill-parent {\n        flex: 1;\n      }\n      \n      p code {\n        background: #eee;\n      }\n      \n      table {\n        border-collapse: collapse;\n        margin: 1em 0;\n      }\n      \n      table, th, td {\n        border: 1px solid black;\n      }\n      \n      th {\n        border-bottom: 4px solid black;\n      }\n      \n      td {\n        padding: 0 4px;\n      }\n      \n      thead > tr {\n        background: #ccc;\n      }\n      \n      .container\n      \n      .col {\n       background: #eee;\n       overflow: scroll;\n       flex: 2;\n      }\n      \n      \n      .col.wide {\n        flex: 4;\n      }\n      .col.narrow {\n        flex: 1;\n      }\n      \n      article {\n        max-width: 960px;\n        min-width: 320px;\n        margin: auto;\n        word-break: break-word;\n        padding: 1rem 8px;\n      }\n      \n      aside {\n        background: #eee;\n        font-size: 0.8rem;\n        flex: 1;\n        max-height: 100vh;\n        overflow: auto;\n        position: sticky;\n        top: 0;\n      }\n      \n      aside:not(.is-narrow) section {\n        column-width: 240px;\n        font-size: 0.8rem;\n        column-count: 2;\n      }\n      \n      aside.is-wide section {\n        column-width: 240px;\n        font-size: 0.9rem;\n        column-count: 3;\n      }\n      \n      .fullwidth {\n         width: 100%;\n      }\n      \n      .collapse {\n        display: none;\n      }\n      \n      .text-small {\n        font-size: 0.8rem;\n      }\n      \n      section {\n      \tscroll-margin-top: var(--navbar-height);\n      }\n      \n      section.sidebar  {\n        position: fixed;\n        bottom: 0px;\n        background: #ccc;\n        overflow: auto;\n        top: var(--navbar-height);\n        z-index: 1000;\n        width: 320px;\n        right: 0;\n      }\n      \n      .col.folded > article {\n        display: none;\n      }\n      \n      \@media screen and (min-width: 960px) {\n        .container {\n          display: flex;\n          flex-direction: row;\n          position: absolute;\n          width: 100%;\n          height: 100%;\n          overflow: hidden;\n        }\n        .container > :not(:first-child) {\n          margin-left: 4px;\n        }\n      \n        .col > .col-header {\n            position: sticky;\n            top: 0;\n        }\n      \n        .col.folded > article {\n          visibility: hidden; /* can not use display none, because layout will be lost, scroll position lost */\n          display: block;\n        }\n      \n        .col.folded > .col-header  {\n          writing-mode: vertical-rl; /* Vertical writing mode, content flows from top to bottom */\n          transform: rotate(180deg); /* Rotate the text 180 degrees */\n          width: 100%; /* Very important to fill the entire col */\n          height: 100%;\n        }\n      \n        .col.folded {\n          overflow: hidden;\n          flex: 0;\n          min-width: 30px;\n       }\n      \n        aside > section {\n          display: block !important;\n        }\n        #toggle-index {\n          display: none;\n        }\n      }\n      \n      pre, pre.prettyprint {\n        padding: 0.5rem !important;\n        font-size: 0.9rem;\n        white-space: pre-wrap;\n      }\n      pre.listing {\n        padding-left: 3rem !important;\n        border: 2px solid black;\n      }\n      \n      .line-number {\n        font-size: 0.7rem;\n        margin-left: -3rem !important;\n        margin-right: 1rem;\n        user-select: none; /*  */\n      }\n      \n      \n      .L-1 {\n       margin-left: 16px;\n      }\n      .L-2 {\n       margin-left: 28px;\n      }\n      .L-3,.L-4,.L-5,.L-6,.L-7,.L-8 {\n       margin-left: 36px;\n      }\n      \n      \n      img {\n        width: 100%;\n      }\n      \@media screen and (min-width: 480px) {\n         img.tiny { width: 50%; }\n      }\n      \n      \@media screen and (min-width: 720px) {\n         img.tiny { width: 33%; }\n         img.small { width: 66%; }\n      }\n      \n      \@media screen and (min-width: 960px) {\n        img.tiny { width: 25%; }\n        img.small { width: 50%; }\n        img.medium { width: 75%; }\n      }\n      \@media print {\n        img.tiny { width: 2.5in; }\n        img.small { width: 5in; }\n      }\n      \n      \n      figure { text-align: center}\n      \n      \@media print {\n        * {\n          /* Don't waste color ink */\n          color: black !important;\n        }\n        \@page {\n          size: auto; /* Magic: disable browser generated footer and header */\n          margin: 1em;\n          table {\n            page-break-inside: avoid;\n            page-break-after: avoid;\n          }\n        }\n        aside { display: none; }\n      }\n      \n    </style>\n  </head>\n  <body>";
  print "<div class=\"xxpad\"><h1>Index</h1>\n";
  print "<table>\n";
  print "<thead><tr class=\"sticky-top\"><th>Name</th><th>Size</th><th>Last Modified</th></tr></thead>\n";
  for (@contents) {
     my $timestr = strftime('%FT%TZ%z', localtime($_->{mtime}));
     print "<tr>";
     print "<td><a href=\"$_->{name}\">$_->{name}</a></td>";
     print "<td class=\"align-right\">$_->{size}</td>";
     print "<td>$timestr</td>";
     print "</tr>\n";
  }
  print "</table></div>";
  print "  </body>\n</html>";
}

sub on_hello {
    my $cgi  = shift;   # CGI.pm object
                            return if !ref $cgi;

    my $name = $cgi->param('name');
    open FH, "perl ./zen2.pl --html  $name|";
    while (<FH>) {
       print $_;
    }
    close FH
}
}

my $server = App::zen::WebServer->new($opt_port);
if ($opt_daemon == 2) {
  $server->background();
} else {
  $server->run();
}
exit;
}

if (scalar @ARGV != 1) {
  usage();
}

my $zenfile = load_zen_file(shift @ARGV);

if ($opt_debug) {
  foreach (@sections) {
    print STDERR "Section [$_->{pattern}] $_->{heading} ", scalar(@{$_->{lines}}), " lines\n";
  }
}

if ($opt_output_file) {
   open $output,">$opt_output_file" or die "Can not write to `$opt_output_file'";
} else {
   $output = *STDOUT;
}

if ($opt_json_dump) {
  print $output sections_to_json(\@sections);
  close $output;
  exit;
}

if ($opt_exec) {
  my $s = find_section($opt_exec);
  my $lang;
  foreach (@{$s->{blocks}}) {
    $lang = $_->{lang};
    last if $lang;
  }
  if ($lang eq 'pl') {
    $lang = 'perl';
  } elsif ($lang eq 'js') {
    $lang = 'node';
  } elsif ($lang eq 'ts') {
    $lang = 'ts-node';
  }
  open $output, "|$lang";
  emit($output, $opt_output_file, expand($opt_exec));
} elsif (!$opt_html && !$opt_latex) {
  if ($opt_source_map) {
    open $output_source_map, ">$opt_source_map" or die "Can not write to source map";
  }
  emit($output, $opt_output_file, expand($opt_target));
  close $output_source_map if $output_source_map;
} else {
  build_refs();
  if ($opt_html) {
    docgen_html($zenfile, $output);
  }
  if ($opt_latex) {
    docgen_latex($output);
  }
}

close $output;
