# dullpencil.py - library classes for Dull Pencil translator
#
#   12/21/24
#   This is version 2 - we are moving to a .ww file instead of .txt.

import os.path
from os import listdir, getcwd
from os.path import isdir, join, abspath
import random

''' (not worth it)
# Warning, we have to install: "pip3 install iptcinfo3"
from iptcinfo3 import IPTCInfo
'''

#from PIL import Image



# DpTranslator: open, read, translate .txt file, return HTML string.
class DpTranslator(object):
  def __init__( self, site, rootDir, isRealTime ):
    self.site = site        # site is the segment after the rootDir
    self.rootDir = rootDir  # directory starting with '/', preceeding 'site'
    self.isRealTime = isRealTime # True if running as web server
    self.id = 1             # used to track mp3shuffle script instances
    self.cssfile = self.relPrefix() + '/' + self.site + '/style.css'

  # relPrefix() - get the prefix we add to an absolute file reference
  # to turn it into a relative reference. Note "current working directory"
  # is relevant.
  def relPrefix( self ):
    # The string we add to the front of an absolute 
    # Change from absolute ("/file") to relative ("../../file") notation
    if hasattr(self,'path'): path = self.path
    else: path = self.rootDir
    aa = path[len(self.rootDir):].split('/')
    if len(aa) <= 1: fix = "."
    elif len(aa) == 2: fix = ".."
    else:
      fix = ".."
      for j in range(len(aa)-2):
        fix += "/.."
    return fix


  # maybeHtml()
  # Optionally change from absolute to relative prefix.
  def maybeHtml( self, fn ):
    if not self.isRealTime:
      # start by converting to a relative reference if needed
      if fn[0] == '/': f2 = self.relPrefix()+fn
      else: f2 = fn
      
      #print('maybeHtml:', fn, f2)
      
      #
      if f2[len(f2)-1] == '/': f2 = f2[:len(f2)-1]
      f2 = f2 + '/index.html'
      if os.path.isfile( f2 ): fn = f2
    return fn


  # preamble(), provides site specific HTML preamble.
  # Subclass this to customize HTML preamble.
  def preamble( self, path ):
    # default HTML preamble
    s = (
      '<!DOCTYPE html>\n'
      '<html>\n'
      '<head>\n'
      '  <meta charset="UTF-8">\n'
      '  <title>' + self.site + '</title>\n'
      '  <link rel="stylesheet" href="' + self.cssfile + '">\n'
      '</head>\n'
      '<body>\n' )
    return s


  # postamble(), provides site specific HTML postamble.
  # Subclass this to customize HTML postamble.
  def postamble( self ):
    # default HTML postamble
    s = '\n</body>\n</html>\n'
    return s







  # wwToHtml() - 
  def wwToHtml( self, fileName ):
    self.fileName = fileName
    self.path = os.path.dirname( fileName )
    self.cssfile = self.relPrefix() + '/' + self.site + '/style.css'
    
    #print( 'wwToHtml():', self.fileName )
    #print( '  root: ', self.rootDir )
    #print( '  fileName: ', self.fileName )
    #print( '  path: ', self.path )
    #print( '  site: ', self.site )
    #print( 'cssfile: ', self.cssfile )
    
    self.surname = ''
    self.re = []  # the list of strings, once joined is returned HTML

    self.env = ''  # string we use to track the current environment

    self.ident = 0
    self.identstr = ''
    self.istable = False
    self.tablestuff = ''
    
    self.pbuf = ''  # buffer used to accumulate paragraph data
  
    #print('opening file:',self.fileName)
  
    try:
      s = open(self.fileName, 'r').read()
    except:
      print( 'Failed to read:', self.fileName )
      return ""
    
    # start with HTML preamble
    '''
    cssfile = '/' + self.site + '/style.css'
    
    # Change from absolute ("/file") to relative ("../../file") notation
    aa = self.path[len(self.rootDir):].split('/')
    if len(aa) <= 1: fix = "."
    elif len(aa) == 2: fix = ".."
    else:
      fix = ".."
      for j in range(len(aa)-2):
        fix += "/.."
    
    cssfile = self.relPrefix() + cssfile
    #
    self.re.append( self.preamble( self.path, cssfile ) )
    '''
    
    # I need to set this up so a "default" preamble comes in if no .css
    # file is available. Or the default preamble() function will take 
    # care of that. Then I wouldn't need the "wiki" directory that is 
    # empty except for the "style.css" file.
    
    self.re.append( self.preamble( self.path ) )

    # If .isHtml, we don't make it a "<p>", and don't do indenting.
    # This is based on whether first char of paragraph is a '<'.
    self.isHtml = False  # flag indicates paragraph started with '<'
   
    # process markup

    
    state = 0  # start as if we had just received a '\n'
    cmd = ['','']
    cmdNest = 0
    bold = False
    italic = False
    iCnt = 0  # italic count
    dCnt = 0  # m-dash count



    #self.emptyLine = False  # default start state
    #self.parEmpty = True   # paragraph has not been written to
    self.isParagraph = False
    self.env = ''

    buf = ''
    i = 0
    while i < len(s):
      c = s[i]
      #
      #
      # state - 0
      # - We are at the start of a new "paragraph" (and new-line).
      # - Paragraphs are separated by an "empty-line" (may have white-space).
      # - In this "state", we determine whether to apply "<p>".
      if state == 0:
        # Make sure "environments" are closed out between "paragraphs".
        if len(self.env) > 0:
          buf = self.adjustEnv( '', buf )
        if self.isParagraph:
          buf += '</p>\n'
          self.isParagraph = False
        
        
        '''
        #
        #
        # If first char is '<', assume whole paragraph is HTML unless it
        # is a <br> or <pre>. Treat <br> & <pre> as embedded.
        # Making this assumption makes it so HTML can be indented
        # through the paragraph without confusing wiki-text indentation.
        if c == '<':
          # If it is a '<br>' or '<pre>', let it be processed as wiki-text.
          skip = False
          if i+4 < len(s) and s[i:i+4] == '<br>': skip = True
          if i+5 < len(s) and s[i:i+5] == '<pre>': skip = True
          if not skip:
            # Just copy it all over through to the end of the paragraph
            nlCnt = 0
            while i < len(s):
              c = s[i]
              buf += c
              if c == '\n': nlCnt += 1
              else: nlCnt = 0
              i += 1
              if nlCnt > 1: break
            continue
        '''
        #
        #
        # If '<br>', '<pre>', or '<!--', treat like they are invisible.
        # Paragraph type determination needs to be based on what follows
        # the '<br>', '<pre>', or '<!--', as if they were not there.
        # This block of code makes it so we can put a '<br>' at the start 
        # of a text paragraph without other consequence. 
        # Check for HTML comment, pre, or break
        if c == '<':
          buf2 = ''
          cnt = 0
          gotOne = False
          # Check for comment
          if i+4 <= len(s) and s[i:i+4] == '<!--':
            buf2,cnt = self.getComment(s,i)  # returns comment & nbr processed
            gotOne = True
          # Look for <pre>
          elif i+5 <= len(s) and s[i:i+5] == '<pre>':            
            buf2,cnt = self.getPre(s,i)
            gotOne = True
          elif i+4 <= len(s) and s[i:i+4] == '<br>':
            buf2 += '<br>'
            cnt = 4
            gotOne = True
          if gotOne:
            buf += buf2
            i += cnt
            continue
            

          
        #
        #
        # Look past the spaces-tabs to find the first character
        sCnt = 0
        while i+sCnt < len(s) and s[i+sCnt] in ' \t': sCnt += 1
        # Skip past if done processing
        if i+sCnt >= len(s):
          buf += s[i:]
          i += sCnt
          continue
        c2 = s[i+sCnt]
        # Check for empty line
        if c2 == '\n':
          buf += s[i:i+sCnt+1]
          i += sCnt+1
          continue
        #
        # First non-space character determines how we handle the paragraph.
        
        
        
        if c2 == '<': self.isHtml = True
        else: self.isHtml = False
        
        
        
        # If command, pre-process the command so we know if it is "embeded".
        buf2 = ''
        cnt = 0
        if c2 == '[':
          # cmd at start of line
          buf2,cnt,embedded = self.getCommand(s,i)
          if not embedded:
            # Not embedded, so process it here, stay in same state.
            buf += buf2
            i += cnt
            continue
          i += cnt
          if i >= len(s):
            continue
          c = s[i]
        # From here we handle all cases of lists, headings, cripple...
        # Note, a list at the start of a "paragraph" will trigger "<p>" 
        # spacing. If not after empty-line, then no paragraph spacing.
        # It is easiest to identify what is not a paragraph.
        elif c2 in '<=\\':
          pass
        elif i+sCnt+4 < len(s) and s[i+sCnt:i+sCnt+4] == '----':
          pass  # doesn't do any good to wrap a <hr> in a <p>

          
        elif self.isHtml:
          pass  # paragraph is an HTML style paragraph

          
        else: self.isParagraph = True
        #
        if self.isParagraph: buf += '<p>'
        if len(buf2) > 0: buf += buf2
        # If we made it to here we are moving on to state 1
        state = 1
      #
      #
      # state 1 - We are at the start of a new-line.
      # - process the environment ('env')
      if state == 1:        
        # Build up newEnv as we go.
        newEnv = ''
        #
        # Count and absorb spaces at start of line.
        sCnt = 0
        while i+sCnt < len(s) and s[i+sCnt] in ' \t': sCnt += 1
        # absorb spaces before further processing
        i += sCnt
        if i >= len(s):
          continue
        c = s[i]
        # Check for empty line
        if c == '\n':
          # if isParagraph, write the '\n' later, it makes the HTML more readable.
          if not self.isParagraph: buf += c
          i += 1
          state = 0
          continue
        # Process "indents"
        sCnt = int(sCnt/2)  # sCnt = the number of indents we need to be at
        # (skip indent handling if self.isHtml
        while sCnt > 0 and not self.isHtml:
          newEnv += ' '
          sCnt -= 1
        #
        # Check for a non-embedded command
        if c == '[':
          # cmd at start of line
          buf2,cnt,embedded = self.getCommand(s,i)
          i += cnt
          if embedded:
            # Adjust env before we write to buffer
            buf = self.adjustEnv( newEnv, buf )
            buf += buf2
            # empty buf before going into state 2
            if len(buf) > 0:
              self.re.append( buf )
              buf = ''
            state = 2  # process rest off the rest of the line
          else:
            # there is no "env" at the start of an non-embedded command
            buf += buf2
            state = 0  # treat it like a paragraph break
          continue
        # Check for and process headers and horz-line ('----', '==')
        elif c == '-':
          if i+4 <= len(s) and s[i:i+4] == '----':
            # we have a horz-line, process it and absorb rest of line
            # adjust env before we write to buffer
            buf = self.adjustEnv( newEnv, buf )
            buf += '<hr>\n'
            # ignore the rest of the line
            while i < len(s) and s[i] != '\n': i += 1
            i += 1  # past '\n'
            state = 0
            continue
        # Cripple
        elif c == '\\':
          # Cripple-paragraph, just save the env and skip past it
          newEnv += '\\'
          #i += 1
        # Definition
        elif c == '%':
          newEnv += c
          # leave the '%' for further processing
        # List (before table)
        elif c in ':*#':
          while i < len(s) and s[i] in ':*#':
            newEnv += s[i]
            i += 1
          #
          # Check for this specific case (':|') of a table embedded in a list.
          # We used this quite a bit in an older wiki-text, so allow for it.
          if i < len(s) and s[i] in '|!': newEnv += '|'
          else: i -= 1  # back up one so it will process the list
        # Table
        elif c in '|!':
          if i+1 <= len(s) and s[i:i+1] == '||':
            pass
          else:
            # we have start of table
            #
            # We used (':|') to burry a table in a list in earlier days.
            # We want to allow for that now, so detect if that is what
            # we are into here.
            if self.env[-2:] == ':|': newEnv += ':|'
            else: newEnv += '|'
        #
        #
        # If we made it to here with no or matching "newEnv", then the new 
        # line is most likely a follow-on line from the last environment.
        # This detects if the new line is a continuation of the last env.
        # If newEnv is empty or all spaces 
        # and self.env is one longer than newEnv
        # and newEnv matches self.env for length of newEnv
        # and last char of self.env is not a space:
        # 
        if len(self.env) == len(newEnv)+1 and self.env[-1] != ' ':
          if newEnv == self.env[:len(newEnv)]:
            if newEnv == '                '[:len(newEnv)]:
              newEnv = self.env
        #
        #
        # It is just a normal "paragraph", so process it
        # Adjust env
        buf = self.adjustEnv( newEnv, buf )
        # We need 'buf' empty before we process the rest of the line
        if len(buf) > 0: self.re.append( buf )
        buf = ''
        state = 2
        # make sure "c" is ready
        continue
      #
      #
      # state 2 - process the rest of the line as a unit
      # - 'buf' must be empty before first enterint this state.
      #   'buf' can be added to as this state is processed.
      # - Processing embedded content:
      #   [[, [], [<, [&, "'", '--', [--, ['..', comment, command
      if state == 2:
        # Check for HTML comment or pre
        if c == '<':
          buf2 = ''
          cnt = 0
          gotOne = False
          # Check for comment
          if i+4 <= len(s) and s[i:i+4] == '<!--':
            buf2,cnt = self.getComment(s,i)  # returns comment & nbr processed
            gotOne = True
          # Look for <pre>
          elif i+5 <= len(s) and s[i:i+5] == '<pre>':            
            buf2,cnt = self.getPre(s,i)
            gotOne = True
          if gotOne:
            buf += buf2
            i += cnt
            continue
        #
        if c != "'" and iCnt > 0:
          if iCnt > 1:
            # we have italic or bold to process
            buf = buf[:-iCnt]
            while iCnt > 1:
              if iCnt == 2:
                # toggle italic
                if italic: buf += '</i>'
                else: buf += '<i>'
                italic = not italic
                iCnt -= 2
              elif iCnt == 3:
                # toggle bold
                if bold: buf += '</b>'
                else: buf += '<b>'
                bold = not bold
                iCnt -= 3
              elif iCnt == 5:
                # toggle bold and italic
                if italic: buf += '</i>'
                else: buf += '<i>'
                italic = not italic
                if bold: buf += '</b>'
                else: buf += '<b>'
                bold = not bold
                iCnt -= 5
              else:
                # we have 4 or 6+ in iCnt
                if italic:
                  # terminate italic
                  buf += '</i>'
                  italic = False
                  iCnt -= 2
                elif bold:
                  # terminate bold
                  buf += '</b>'
                  bold = False
                  iCnt -= 3
                else:
                  # start italic and loop (makes no sense...)
                  buf += '<i>'
                  italic = True
                  iCnt -= 2
                #
              #
            #
            if iCnt > 0: buf += "'"
          iCnt = 0
        if c != "-" and dCnt > 0:
          # must be exactly two dashes and not HTML end of comment
          if dCnt == 2 and c != '>':
            # must not be HTML start of comment
            if not(len(buf) > 3 and buf[-4:2] == '<!'):
              # we have two short dashes that are not part of other command
              buf = buf[:-dCnt]
              buf += '&mdash;'
            #
          #
          dCnt = 0
        if c == '\n':
          # new-line, so time to go back to state 0
          buf += c
          # process list and table stuff that is in "buf"
          j = 0
          if j < len(buf):
            s2 = ''
            c2 = buf[j]
            if c2 in ':*#\\':
              s4 = buf[1:].strip()
              # Sometimes we use an empty list item to force horizontal
              # spacing. It only works if we add in a "&nbsp;"
              if len(s4) == 0: s4 = '&nbsp;'
              s2 += s4
            elif c2 == '%':
              # Do stuff to process a "definition"
              if len(buf) > 1:
                s3 = buf[1:].strip()
                k = s3.find(':')
                k2 = s3.find('-')
                if k < 0: k = k2
                elif k2 >= 0:
                  if k2 < k: k = k2
                if k > 0:
                  if s3[k] == '-': k -= 1
                  k += 1
                  s2 += '<b>'+s3[:k]+'</b>'
                  if k+1 < len(s3): s2 += s3[k:]
                else:
                  if k < 0: k = 0
                  else: k = 1
                  s2 += s3[k:].strip()
            elif c2 in '|!':
              # we have a table element
              if buf[0:2] != '||':
                # start of new row
                if c2 == '!': self.isheader = True
                else: self.isheader = False
                if not self.istable:
                  # start of table

                  #print( '0:', buf )

                  self.istable = True
                  s2 += '\n<tr>\n'
                else:
                  if self.isheader: s2 += '</th>\n'
                  else: s2 += '</td>\n'
                  s2 += '</tr>\n<tr>\n'
                if self.isheader: s2 += '<th>'
                else: s2 += '<td>'
                buf = buf[1:]
              a = buf.split('||')
              for k in range(0, len(a)-1):
                if self.isheader: s2 += a[k] + '</th>\n<th>'
                else: s2 += a[k] + '</td>\n<td>'
                
              #print( 'this: "%s"'% (a[-1]) )
                
              s2 += a[-1]
            elif j+2 < len(buf) and buf[j:j+2] == '==':
              # Header
              # form up another string out of what is on this line
              b2 = ''
              k = j
              while k < len(buf) and buf[k] != '\n': 
                b2 += buf[k]
                k += 1
              s3 = ''
              level = 2
              while level < len(b2) and b2[level] == '=':
                level += 1
              c3 = b2[:level]
              a2 = b2[level:].strip().split(c3)
              c4 = str(level)
              s3 += '<h' + c4 + '>' + a2[0].strip() + '</h' + c4 + '>\n'
              if level == 2: s3 += '<hr>\n'
              if len(a2) > 1:
                b2 = a2[1].strip()
                if len(b2) > 0:
                  # tack on the left-overs
                  s3 += b2
                #
              s2 += s3
              # Treat a header as if it is followed by paragraph break
              if len(s2) > 0: buf = s2
              i += 1
              state = 0
              continue
            #
            if len(s2) > 0: buf = s2
          #
          i += 1
          state = 1
        elif c == '[':
          # Process a command
          buf2,cnt,embedded = self.getCommand(s,i)
          buf += buf2
          i += cnt
          continue
        else:
          buf += c
          if c == "'": iCnt += 1
          if c == '-': dCnt += 1
          i += 1
        #
      #
    # end while()

    # Flush the buffer
    if len(buf) > 0:
      self.re.append( buf )
      buf = ''
    # terminate things that need it
    if bold:
      self.re.append( '</b>' )
      bold = False
    if italic:
      self.re.append( '</i>' )
      italic = False
      
    # Close the environments.
    while len(self.env) > 0: buf = self.closeEnv( buf )
    
    # close paragraph
    if self.isParagraph:
      self.re.append( '</p>' )
      self.isParagraph = False
    
    if cmdNest > 0:
      self.re.append( '\n<p>Error: unterminated command somewhere!</p>\n' )
    
    # add in the postamble for HTML
    self.re.append( self.postamble() )
    
    return ''.join(self.re)




  def adjustEnv( self, newEnv, buf ):
    
    #print( 'adjustEnv: "%s" "%s"'% (newEnv, self.env) )
    
    # Start by closing the environments we are done with.
    while len(self.env) > len(newEnv): buf = self.closeEnv( buf )
    #
    while len(self.env) > 0:
      if self.env == newEnv[:len(self.env)]:
        if len(self.env) == len(newEnv):
          #if self.env == 'p' and not self.parEmpty:
          #if self.env == 'p':
          #  #buf = self.closeEnv( buf )
          #  pass
          if self.env[-1] in '\\':
            buf = self.closeEnv( buf )
            pass
          elif self.env[-1] == ':':
            # continuation
            buf += '</dd>\n<dd>'
          elif self.env[-1] in '*#':
            # continuation
            buf += '</li>\n<li>'
          elif self.env[-1] == '%':
            # continuation
            buf = self.closeEnv( buf )
        break
      buf = self.closeEnv( buf )
    
    # now that we've processed this, re-trigger the flag
    self.emptyLine = False

    # Open needed new environments
    while len(self.env) < len(newEnv):
      c2 = newEnv[len(self.env)]
      #if c2 == 'p': buf += '\n<p>'
      if c2 == '\\': buf += '<cripple>'      
      elif c2 == ':': buf += '<dl><dd>\n'
      elif c2 == '*': buf += '<ul><li>\n'
      elif c2 == '#': buf += '<ol><li>\n'
      elif c2 in '|!':
        if len(self.tablestuff) > 0: buf += '<table '+self.tablestuff+'>'
        else: buf += '<table>'
        c2 = '|'
      elif c2 == '%': buf += '<div class="definition">\n'
      else: 
        #buf += '<div style="margin-left:1em;">\n'
        buf += '<div class="indent">\n'
        c2 = ' '
      self.env += c2
      
    return buf


  def getComment( self, s, i ):
    # to start with, s[i] == '<' with '<!--' verified
    buf = ''
    j = s[i:].find('-->')
    if j > 0: cnt = j+3
    else: cnt = len(s) - i
    buf = s[i:i+cnt]
    return (buf,cnt)


  def getPre( self, s, i ):
    #buf2,cnt = self.getPre(s,i)
    buf = ''
    j = s[i:].find('</pre>')
    if j > 0: cnt = j+6
    else: cnt = len(s) - i
    buf = s[i:i+cnt]
    return (buf, cnt)


  def getCommand( self, s, i ):
    # at this point, s[i] == '['
    buf = ''
    cnt = 1  # s[i+cnt] would be one past the cmd close
    embedded = True
    cmd = ['','']
    cmdNest = 0
    # Check for these: [[, [], [<, [&, [--, or [''
    if i+cnt < len(s):
      c = s[i+cnt]
      if c in '[]':
        buf += c
        cnt += 1
      elif c == '<':
        buf += '&lt;'
        cnt += 1
      elif c == '&':
        buf += '&amp;'
        cnt += 1
      elif c == '-':
        while i+cnt < len(s) and s[i+cnt] == '-': 
          buf += c
          cnt += 1
      elif c == "'":
        while i+cnt < len(s) and s[i+cnt] == "'": 
          buf += c
          cnt += 1
      else:
        # it is a full command
        cmdNest = 1
        cmd[cmdNest] = '['
        while i+cnt < len(s):
          c = s[i+cnt]
          # parsing command, we have already seen '['
          if c == '[':
            cmdNest += 1
            try:
              cmd[cmdNest] = ''
            except IndexError:
              cmd.append( '' )
            cmd[cmdNest] += c
          elif c == ']':
            cmd[cmdNest] += c
            s2,embedded = self.doCmd2( cmd[cmdNest] )
            cmdNest -= 1
            if cmdNest == 0:
              buf += s2
              break
            else:
              cmd[cmdNest] += s2
            #
          else:
            cmd[cmdNest] += c
          #
          #
          cnt += 1
        #
        cnt += 1
      #
    #
    return (buf, cnt, embedded)




      



  
  # toHtml(): open, read, translate .txt file, return HTML string.
  #def toHtml( self ):
  def toHtml( self, fileName ):
    self.fileName = fileName
    self.path = os.path.dirname( fileName )
    self.cssfile = self.relPrefix() + '/' + self.site + '/style.css'
    
    #print( 'toHtml():' )
    #print( '  root: ', self.rootDir )
    #print( '  fileName: ', self.fileName )
    #print( '  path: ', self.path )
    #print( '  site: ', self.site )
    #print( 'cssfile: ', self.cssfile )
    
    self.surname = ''
    self.re = []  # the list of strings, once joined is returned HTML

    self.ident = 0
    self.identstr = ''
    self.istable = False
    self.tablestuff = ''
  
    #print('opening file:',self.fileName)
  
    try:
      s = open(self.fileName, 'r').read()
    except:
      print( 'Failed to read:', self.fileName )
      return ""
    
    # start with HTML preamble
    '''
    cssfile = '/' + self.site + '/style.css'
    
    # Change from absolute ("/file") to relative ("../../file") notation
    aa = self.path[len(self.rootDir):].split('/')
    if len(aa) <= 1: fix = "."
    elif len(aa) == 2: fix = ".."
    else:
      fix = ".."
      for j in range(len(aa)-2):
        fix += "/.."
    
    cssfile = self.relPrefix() + cssfile
    #
    self.re.append( self.preamble( self.path, cssfile ) )
    '''
    self.re.append( self.preamble( self.path ) )
   
    # process markup
    state = 0  # start as if we just received a '\n'
    buf = ''
    cmd = ['','']
    cmdNest = 0
    bold = False
    italic = False
    iCnt = 0
    dCnt = 0
    i = 0
    while i < len(s):
      c = s[i]
      if state == 0:
        if c != "'" and iCnt > 0:
          if iCnt > 1:
            # we have italic or bold to process
            buf = buf[:-iCnt]
            while iCnt > 1:
              if iCnt == 2:
                # toggle italic
                if italic: buf += '</i>'
                else: buf += '<i>'
                italic = not italic
                iCnt -= 2
              elif iCnt == 3:
                # toggle bold
                if bold: buf += '</b>'
                else: buf += '<b>'
                bold = not bold
                iCnt -= 3
              elif iCnt == 5:
                # toggle bold and italic
                if italic: buf += '</i>'
                else: buf += '<i>'
                italic = not italic
                if bold: buf += '</b>'
                else: buf += '<b>'
                bold = not bold
                iCnt -= 5
              else:
                # we have 4 or 6+ in iCnt
                if italic:
                  # terminate italic
                  buf += '</i>'
                  italic = False
                  iCnt -= 2
                elif bold:
                  # terminate bold
                  buf += '</b>'
                  bold = False
                  iCnt -= 3
                else:
                  # start italic and loop (makes no sense...)
                  buf += '<i>'
                  italic = True
                  iCnt -= 2
                #
              #
            #
            if iCnt > 0: buf += "'"
          iCnt = 0
        if c != "-" and dCnt > 0:
          # must be exactly two dashes and not HTML end of comment
          if dCnt == 2 and c != '>':
            # must not be HTML start of comment
            if not(len(buf) > 3 and buf[-4:2] == '<!'):
              # we have two short dashes that are not part of other command
              buf = buf[:-dCnt]
              buf += '&mdash;'
            #
          #
          dCnt = 0
        if c == '\n':
          #s2 = buf.strip()
          if len(buf) > 0:
            buf += c
            state = 1
          # else - ignore '\n' while buf is empty
        elif c == '[':
          cmdNest = 1
          cmd[cmdNest] = '['
          state = 3
        else:
          buf += c
          if c == "'": iCnt += 1
          if c == '-': dCnt += 1
      elif state == 1:
        # newline was detected w/non-empty buf
        if c == '\n':
          # end of paragraph detected
          self.re.append( self.doParagraph( buf ) )
          # terminate lists, etc.
          # tables can be nested inside lists, so terminate tables first
          if self.istable:
            if self.isheader: s2 = '</th>\n'
            else: s2 = '</td>\n'
            self.re.append( s2 + '</tr>\n</table>\n' )
            self.istable = False
          #
          if self.ident > 0: self.re.append( self.unindent( 0 ) )
          #
          buf = ''
          state = 0
        elif c in ':*#|!':
          # end of paragraph detected
          self.re.append( self.doParagraph( buf ) )
          buf = c
          state = 0
        elif c == '-':
          ### (the '-<' sequence is a "non-paragraph" cmd for HTML, I think
          ### from an earlier version of dull pencil. I don't think it is needed)
          #if s[i:i+4] == '----' or s[i:i+2] == '-<':
          if s[i:i+4] == '----':
            # end of paragraph detected
            self.re.append( self.doParagraph( buf ) )
            buf = c
          else:
            buf += c
          dCnt += 1
          state = 0
        elif c == '=':
          if s[i:i+2] == '==':
            # end of paragraph detected
            self.re.append( self.doParagraph( buf ) )
            buf = c
          else:
            buf += c
          state = 0
        elif c == '[':
          cmdNest = 1
          cmd[cmdNest] = '['
          state = 3
        else:
          buf += c
          if c == "'": iCnt += 1
          state = 0
      elif state == 3:
        # parsing command
        if c == '[':
          cmdNest += 1
          try:
            cmd[cmdNest] = ''
          except IndexError:
            cmd.append( '' )
          cmd[cmdNest] += c
        elif c == ']':
          cmd[cmdNest] += c
          s2 = self.doCmd( cmd[cmdNest] )
          cmdNest -= 1
          if cmdNest == 0:
            buf += s2
            state = 0
          else:
            cmd[cmdNest] += s2
          #
        elif c == '-' and cmd[cmdNest] == '[-':
          # detected a quoted pre
          cmd[cmdNest] += c
          state = 4
        else:
          cmd[cmdNest] += c
        #
      elif state == 4:
        # parsing a quoted pre command '[-- ... --]'
        if c == ']' and cmd[cmdNest][-2:] == '--':
          # end detected, process quoted characters
          s2 = '<pre>' + cmd[cmdNest][3:-2] + '</pre>'
          cmdNest -= 1
          if cmdNest == 0:
            buf += s2
            state = 0
          else:
            cmd[cmdNest] += s2
            state = 3
        elif c == '<': cmd[cmdNest] += '&lt;'
        elif c == '&': cmd[cmdNest] += '&amp;'
        else: cmd[cmdNest] += c
        #
      #
      i += 1
    #
    if len(buf) > 0:
      self.re.append( self.doParagraph( buf ) )
      #re.append( buf )
      
    if bold:
      self.re.append( '</b>' )
      bold = False
    if italic:
      self.re.append( '</i>' )
      italic = False
    
    # tables can be nested inside lists, so terminate tables first
    if self.istable:
      if self.isheader: s2 = '</th>\n'
      else: s2 = '</td>\n'
      self.re.append( s2 + '</tr>\n</table>\n' )
      self.istable = False
        
    if self.ident > 0: self.re.append( self.unindent( 0 ) )
    
    if cmdNest > 0:
      self.re.append( '\n<p>Error: unterminated command somewhere!</p>\n' )
    
    # add in the postamble for HTML
    self.re.append( self.postamble() )
    
    return ''.join(self.re)


  # doCmd2(): process a [] style command.
  def doCmd2( self, cmd ):
    embedded = True
    self.doCmdDone = False
    s2 = ''
    cmd2 = cmd.strip('[]').strip()
    a = cmd2.split(':')
    if len(a) > 1:
      # there is a command
      cmdArgs = cmd2[len(a[0])+1:].strip()
      c = a[0].strip().lower()
      
      #print( 'doCmd2(): "%s", "%s"'%( cmdArgs, cmd2 ) )
      
      if c in ['head','navpath','chidren','table','img','gallery','dir']:
        embedded = False
      # commands
      if   c == 'head':       s2 = self.head2( cmdArgs )
      elif c == 'navpath':    s2 = self.navpath2()
      elif c == 'person':     s2 = self.person( cmdArgs )
      elif c == 'couple':     s2 = self.couple( cmdArgs )
      elif c == 'children':   s2 = self.children( cmdArgs )
      elif c == 'table':      s2 = self.table2( cmdArgs )
      elif c == 'img':        s2 = self.img2( cmdArgs )
      elif c == 'gallery':    s2 = self.gallery( cmdArgs )
      elif c == 'dir':        s2 = self.dir2( cmdArgs )
      elif c == 'mp3':        s2 = self.mp32( cmdArgs )
      elif c == 'mp3shuffle': s2 = self.mp3shuffle2( cmdArgs )
      elif c == 'http' or c == 'https': s2 = self.http2( cmd2 )
      #elif c == 'html':
      #  # 'html' command: pass all data through w/o translation
      #  s2 = cmd[cmd.find(':')+1:cmd.rfind(']')]
      #  self.doCmdDone = True
      else: self.doCmdDone = False
    else:
      # no command, so assume it is a link to local file
      s2 = self.localFile( cmd2 )
    if not self.doCmdDone:
      s2 += '<em><s>' + cmd + '</s></em>'
      
    #print( 'doCmd2(): "%s"'%( s2 ) )
    
    return (s2, embedded)


  # doCmd(): process a [] style command.
  def doCmd( self, cmd ):
    s2 = ''
    cmd2 = cmd.strip('[]').strip()
    a = cmd2.split(':')
    if len(a) > 1:
      # there is a command
      cmdArgs = cmd2[len(a[0])+1:].strip()
      c = a[0].strip().lower()
      
      # commands
      if   c == 'head':       s2 = self.head( cmdArgs )
      elif c == 'navpath':    s2 = self.navpath()
      elif c == 'person':     s2 = self.person( cmdArgs )
      elif c == 'couple':     s2 = self.couple( cmdArgs )
      elif c == 'children':   s2 = self.children( cmdArgs )
      elif c == 'table':      s2 = self.table( cmdArgs )
      elif c == 'img':        s2 = self.img( cmdArgs )
      elif c == 'gallery':    s2 = self.gallery( cmdArgs )
      elif c == 'dir':        s2 = self.dir( cmdArgs )
      elif c == 'mp3':        s2 = self.mp3( cmdArgs )
      elif c == 'mp3shuffle': s2 = self.mp3shuffle( cmdArgs )
      elif c == 'http' or c == 'https': s2 = self.http( cmd2 )
    else:
      # no command, so assume it is a link to local file
      s2 = self.localFile( cmd2 )
    if len(s2) == 0:
      s2 = '<em><s>' + cmd + '</s></em>'
    return s2


  # doParagraph(): translate a paragraph.
  def doParagraph( self, b ):
    s2 = ''
    n = len(b)
    if n > 0:
      c2 = b[0]
      # Since tables can be nested in lists, end tables before ending lists
      if self.istable and c2 not in '!|' :
        # finish off table
        if self.isheader: s2 += '</th>\n'
        else: s2 += '</td>\n'
        s2 += '</tr>\n</table>\n'
        self.istable = False
      if self.ident > 0 and c2 not in ':*#!|' : s2 += self.unindent( 0 )
      if c2 in ':*#':
        cnt = 1
        while len(b) > cnt and b[cnt] in ':*#':
          cnt += 1
        if self.ident > cnt: s2 += self.unindent( cnt )
        if self.ident == cnt:
          c3 = b[cnt-1]
          if self.identstr[-1] != c3:
            s2 += self.unindent( cnt-1 )
          else:
            if c3 == ':':
              s2 += '</dd>\n<dd>'
            else:
              s2 += '</li>\n<li>'
        if self.ident < cnt:
          s2 += '\n'
          while self.ident < cnt:
            c3 = b[self.ident]
            self.identstr += c3
            if c3 == ':':
              s2 += '<dl><dd>'
            elif c3 == '*':
              s2 += '<ul><li>'
            else:
              s2 += '<ol><li>'
            self.ident += 1
        # see if we are starting a table here    
        s3 = b[cnt:].strip()
        if len(s3) > 0 and s3[0] in '!|': s2 += self.doParagraph( s3 )
        else: s2 += s3
      elif c2 in '|!':
        if b[0:2] != '||':
          # start of new row
          if c2 == '!': self.isheader = True
          else: self.isheader = False
          if not self.istable:
            # start of table
            self.istable = True
            s2 += '<table ' + self.tablestuff + '>\n<tr>\n'
          else:
            if self.isheader: s2 += '</th>\n'
            else: s2 += '</td>\n'
            s2 += '</tr>\n<tr>\n'
          if self.isheader: s2 += '<th>'
          else: s2 += '<td>'
          b = b[1:]
        a = b.split('||')
        for i in range(0, len(a)-1):
          if self.isheader: s2 += a[i] + '</th>\n<th>'
          else: s2 += a[i] + '</td>\n<td>'
        s2 += a[-1]
      #### (I can't tell that this "non-paragraph" command is needed) ####
      #elif b[:2] == '-<':
      #  # the non-paragraph cmd just leaves off the starting dash
      #  s2 += b[1:]
      elif b[:4] == '----':
        s2 += '<hr>\n'
        b = b[4:].strip('\n ').strip()
        if len(b) > 0:
          s2 += self.doParagraph( b )
      elif b[:2] == '==':
        level = 2
        while len(b) > level and b[level] == '=':
          level += 1
        c2 = b[:level]
        a2 = b[level:].strip().split(c2)
        c3 = str(level)
        s2 += '<h' + c3 + '>' + a2[0] + '</h' + c3 + '>\n'
        if level == 2:
          s2 += '<hr>\n'
        if len(a2) > 1:
          b2 = a2[1].strip()
          if len(b2) > 0:
            s2 += self.doParagraph( b2 )
      else:
        s2 += '<p>' + b.strip() + '\n</p>\n'
    else:
      s2 += 'We have a problem'
    return s2



  # flushBuf()
  def flushBuf( self ):
    if len(self.pbuf) > 0:
      s2 = ''
      n = len(self.pbuf)
      c2 = self.pbuf[0]
      if c2 == ':':
        s2 += '<dd>'
        
        s3 = self.pbuf[1:].strip()
        #if s3[:2] == '<a':
          
          #print( s3 )
        
        if len(s3) > 0: s2 += s3
        #if len(self.pbuf) > 1: s2 += self.pbuf[1:].strip()
        s2 += '</dd>\n'
      elif c2 in '*#':
        s2 += '<li>'
        if len(self.pbuf) > 1: s2 += self.pbuf[1:].strip()
        s2 += '</li>\n'
      elif c2 in '|!':
        # we have a table element
        if self.pbuf[0:2] != '||':
          # start of new row
          if c2 == '!': self.isheader = True
          else: self.isheader = False
          if not self.istable:
            # start of table
            self.istable = True
            s2 += '\n<tr>\n'
          else:
            if self.isheader: s2 += '</th>\n'
            else: s2 += '</td>\n'
            s2 += '</tr>\n<tr>\n'
          if self.isheader: s2 += '<th>'
          else: s2 += '<td>'
          self.pbuf = self.pbuf[1:]
        a = self.pbuf.split('||')
        for i in range(0, len(a)-1):
          if self.isheader: s2 += a[i] + '</th>\n<th>'
          else: s2 += a[i] + '</td>\n<td>'
        s2 += a[-1]
      elif n >= 4 and self.pbuf[:4] == '----':
        # we have a horizontal line
        s2 += '<hr>\n'
      elif self.pbuf[:2] == '==':
        level = 2
        while len(self.pbuf) > level and self.pbuf[level] == '=':
          level += 1
        c2 = self.pbuf[:level]
        a2 = self.pbuf[level:].strip().split(c2)
        c3 = str(level)
        s2 += '<h' + c3 + '>' + a2[0] + '</h' + c3 + '>\n'
        if level == 2:
          s2 += '<hr>\n'
        if len(a2) > 1:
          b2 = a2[1].strip()
          if len(b2) > 0:
            # tack on the left-overs
            s2 += b2
            #s2 += self.doParagraph2( b2 )
          #
        #
      #
      elif self.pbuf[:5] == '<pre>':
        # intercept <pre> command to avoid being wrapped in a paragraph
        s2 += self.pbuf
      else:
        # See if we need to add paragraph env
        if len(self.env) == 0 or self.env[-1] != 'p':
          s2 += '<p>'
          #self.re.append( '<p>\n' )
          self.env += 'p'
        #
        s2 += self.pbuf.strip()
      #
      self.re.append( s2 )
      self.pbuf = ''
    #

  
  def closeEnv( self, buf ):
    if len(buf) > 0: self.re.append( buf )
    if len(self.env) > 0:
      e = self.env[-1]
      if e == '\\': self.re.append( '\n</cripple>' )
      elif e == ' ': self.re.append( '\n</div>\n' )
      elif e == '%': self.re.append( '\n</div>\n' )
      elif e == ':': self.re.append( '</dd></dl>\n' )
      elif e == '*': self.re.append( '</li></ul>\n' )
      elif e == '#': self.re.append( '</li></ol>\n' )
      elif e == '|':
        if self.istable: 
          if self.isheader: s2 = '</th>\n'
          else: s2 = '</td>\n'
          s2 += '</tr>'
        else: s2 = ''
        self.re.append( s2+'</table>\n' )
        self.istable = False
      #
      if len(self.env) > 1: self.env = self.env[:-1]
      else: self.env = ''
    #
    return ''
  





  # unindent()
  def unindent( self, level ):
    s = ''
    while self.ident > level:
      self.ident -= 1
      c = self.identstr[self.ident]
      if c == ':':
        s += '</dd></dl>'
      elif c == '*':
        s += '</li></ul>'
      else:
        s += '</li></ol>'
      self.identstr = self.identstr[:-1]
    if level == 0: s += '\n'
    return s
    

  # head2()
  def head2( self, argstr ):
    self.flushBuf()
    html = ''
    #html = self.navpath()
    html += '<h1>' + argstr + '</h1>\n<hr>'
    self.re.append( html )
    self.doCmdDone = True
    return ''


  # head()
  def head( self, argstr ):
    html = ''
    #html = self.navpath()
    html += '<h1>' + argstr + '</h1>\n<hr>\n'
    return html


  # navpath2(), generate HTML for navigation path links
  def navpath2( self ):
    self.flushBuf()
    html = ''
    a = self.path[len(self.rootDir)+1:].split('/')
    if len(a) > 0:
      html = '<div style="float:right;">\n'
      if len(a) > 1:
        s3 = '..'
        for i in range(0, len(a)-2):
          s3 += '/..'
        for i in range(0, len(a)-1):
          html += '/ <a href="' + s3 + '">' + a[i] + '</a>\n'
          s3 = s3[:-3]
      #html += '/ <a href=".">' + a[-1] + '</a>\n' + '</div>\n'
      html += '</div>\n'
      self.re.append( html )
      self.doCmdDone = True
    return ''
  
  
  # navpath(), generate HTML for navigation path links
  def navpath( self ):
    html = ''
    a = self.path[len(self.rootDir)+1:].split('/')
    if len(a) > 0:
      html = '<div style="float:right;">\n'
      if len(a) > 1:
        s3 = '..'
        for i in range(0, len(a)-2):
          s3 += '/..'
        for i in range(0, len(a)-1):
          html += '/ <a href="' + s3 + '">' + a[i] + '</a>\n'
          s3 = s3[:-3]
      #html += '/ <a href=".">' + a[-1] + '</a>\n' + '</div>\n'
      html += '</div>\n'
    return html
  
  
  # person()
  def person( self, argstr ):
    self.doCmdDone = True
    return self.couple( argstr )


  # couple()
  def couple( self, argstr ):
    # turns out, this kind (genNavPath()) of nav is useless in 
    # the family page, so leave it out.
    #html = self.genNavPath()
    html = ''
    args = argstr.split(',')
    cnt = len(args)
    if cnt > 1:
      self.surname = args[1].strip()
      html += '<h1>' + args[0].strip() + ' <i>' + args[1].strip() + '</i>'
      for i in range(2,cnt):
        if i == 2: html += ' (' + args[i].strip()
        else: html += ', ' + args[i].strip()
      if cnt > 2: html += ')'
      html += '</h1>\n'
    else:
      html += '<h1>' + argstr + '</h1>\n'
    html += '<hr>\n'
    self.doCmdDone = True
    return html


  # children()
  def children(self, argstr ):
    html = '<p>Children:</p>\n<dl><dd><table>'
    args = argstr.strip().split('\n')
    for i in range(0,len(args)):
      ps = args[i].strip().split('(')
      html += '\n<tr>'
      if len(ps) > 0:
        a = ps[0].strip().split(',')
        if len(a) > 0:
          p = a[0] + ', ' + self.surname
        else:
          p = self.surname
        for i in range(1, len(a)):
          p += ', ' + a[i]
        html += '<td>' + self.localFile( p ) + '</td>\n'
        if len(ps) > 1:
          p2 = self.localFile( ps[1].strip(')').strip() )
          html += '<td>(' + p2 + ')</td>\n'
        else:
          html += '<td></td>\n'
      html += '</tr>'
    html += '\n</table></dd></dl>\n'
    self.doCmdDone = True
    return html


  # table2()
  def table2( self, argstr ):
    self.tablestuff = argstr
    self.doCmdDone = True
    return ''


  # table()
  def table( self, argstr ):
    self.tablestuff = argstr
    
    #print( 'tablestuff: "' + self.tablestuff + '"' )

    """
    html = ''
    row = []
    parms = ''
    s1 = ''
    gotParms = False
    for s in argstr.split('\n'):
      s = s.strip()
      if len(s) > 0 and s[0] in '!|' and s[:2] != '||':
        # start of new row detected
        if not gotParms:
          parms = s1
          gotParms = True
          s1 = s
        else:
          if len(s1) > 0:
            row.append( s1 )
            s1 = s
      else:
        s1 += s
    if not gotParms:
      parms = s1
    else:
      if len(s1) > 0: row.append( s1 )
    if len(row) > 0:
      html += '<table ' + parms + '>\n'
      for s in row:
        c = s[0]
        a = s[1:].split('||')
        html += '<tr>\n'
        for s2 in a:
          if c == '!':
            html += '<th>' + s2 + '</th>\n'
          else:
            html += '<td>' + s2 + '</td>\n'
        html += '</tr>\n'
      html += '</table>\n'
    else:
      html = '<b>"' + argstr + '"</b>'
    return html
    """
    
    return '\n'


  # img2()
  def img2( self, argstr ):
    #self.flushBuf()
    s2 = self.img( argstr )
    #self.re.append( s2 )
    self.doCmdDone = True
    return s2


  # img()
  def img(self, argstr ):

    args = argstr.strip().split('|')
    cap = 0
    px = '150px'
    left = False
    right = False
    # to find caption, look for the first non-usable parameter
    for i in range(0,len(args)):
      args[i] = args[i].strip()
      
      #print( 'IMG(' + str(i) + '): "' + args[i] + '"')

      if args[i][-2:] == 'px' or args[i][-1:] == '%':
        px = args[i]
        
        #print( 'Px: ' + px )
        
      elif args[i] == 'left':
        left = True
      elif args[i] == 'right':
        right = True
      elif i > 0:
        cap = i
          
        #print( 'Caption: ' + args[i] )
          
    if len(args[0]) > 0:
      s2 = ''
      if cap > 0:
        s2 += '<figure style="width:'+px+';display:inline-block;'
        if left: s2 += 'float:left;'
        elif right: s2 += 'float:right;'
        s2 += '">\n'
      s2 += '<a href="' + args[0] + '">'
      if left:
        s2 += '<img src="'+ args[0] + '" '
        s2 += 'style="float:left;margin-right:10px;'
      elif right:
        s2 += '<img src="' + args[0] + '" '
        s2 += 'style="float:right;margin-left:10px;'
      else:
        s2 += '<img src="' + args[0] + '" style="'
      if cap == 0: s2 += 'max-width:'+px+';max-height:'+px+';">'
      else: s2 += 'max-width:100%;max-height=100%;">'
      s2 += '</a>'
      if cap > 0:
        s2 += '<figcaption>' + args[cap] + '</figcaption>\n</figure>'
    else:
      s2 = '\n[img: parameter error...] (' + argstr + ')\n'
    return s2

  '''
  # gallery2()
  def gallery2( self, argstr ):
    #self.flushBuf()
    s2 = self.gallery( argstr )
    #self.re.append( s2 )
    self.doCmdDone = True
    return s2
  '''


  # gallery()
  def gallery(self, argstr ):
    args = argstr.strip().split('\n')
    s2 = '\n'
    s2 += '<center>\n'
    if len(args) > 0:
      for i in range(0,len(args)):
        a = args[i].strip().split('|')
        if len(a) > 0:
          fn = a[0].strip()
        else:
          fn = ''
        if len(a) > 1:
          title = a[1].strip()
        else:
          title = ''
        # The .css file is critical for setting up figure.
        s2 += '<figure>\n'
        if len(fn) > 0:
          s2 += '<a href="' + fn + '">\n'
          s2 += '<img src="' + fn + '" style=width:100%;>\n'
          s2 += '</a>'
        if len(title) > 0:
          # Use the specified caption if provided
          s2 += '<figcaption>' + title + '</figcaption>\n'
        '''
        else:
          # See if we can pick up a caption from the IPTC data in file
          ffn = self.path + '/' + fn
          try:
            info = IPTCInfo( ffn )
            # This is where Shotwell stores the caption.
            cap = info['caption/abstract']
            #print( info['keywords'] ) # tags
            if cap != None:
              s2 += '<figcaption>' + cap.decode() + '</figcaption>\n'
          except Exception as e:
            print( str(e) )
        '''
        s2 += '</figure>\n'
      #
    #
    s2 += '</center>\n'
    # If figures float, text that follows the gallery will wrap up 
    # around the gallery photos. This "clearing div" fixes it.
    #s2 += '<div style="clear:both;"></div>\n'
    self.doCmdDone = True
    return s2


  # dir2()
  def dir2( self, argstr ):
    self.flushBuf()
    s2 = self.dir( argstr )
    self.re.append( s2 )
    self.doCmdDone = True
    return ''


  # dir()
  def dir( self, args ):
    if len(args) <= 0:
      args = '.'
    if args[0] == '/':
      fn = self.rootDir + args
    else:
      fn = self.path + '/' + args
    fn = abspath( fn )
    locFn = fn[len(self.rootDir):] + '/'
    s2 = ''
    if len(fn) >= len(self.rootDir):
      s2 = '<p><em>Directory: ' + locFn + '</em></p>\n'
      if os.path.isdir( fn ):
        s2 += '<dl><dd><table>'
        files = listdir( fn )
        #
        def myFunc(e):
          return e.lower()
        files.sort(key=myFunc)
        
        # Two passes to list directories, then files
        #for i in range(2):
        #(for now, just list them all together)
        for i in range(2):
        
          for f in files:
            # hidden files start with '.', skip them
            if len(f) > 0 and f[0] != '.':
              fn2 = self.rootDir + locFn + f
              
              # list directories first, then files
              #t = isdir(fn2)
              #if (i == 0 and t) or (i == 1 and not t):
              
              # (for now, just sort files and directories together)
              if i == 0:
              
                s2 += '\n<tr>'
                s2 += '<td><a href="' + locFn + f + '">' + f + '</a></td>\n'
                s2 += '</tr>'
              #
            #
          #
        s2 += '\n</table></dd></dl>\n'
      #
    return s2


  # mp32()
  def mp32( self, argstr ):
    #self.flushBuf()
    s2 = self.mp3( argstr )
    #self.re.append( s2 )
    self.doCmdDone = True
    return s2


  #define mp3()
  def mp3( self, argstr ):
    html = '<i><strike>' + argstr + '</strike></i>\n'
    if argstr[0] == '/':
      fn = self.rootDir + argstr
    else:
      fn = self.path + '/' + argstr
    if os.path.isfile( fn ) :
      html = '<audio controls preload="metadata"><source src="' + argstr + '" type="audio/mpeg"></audio>'
    return html


  # mp3shuffle2()
  def mp3shuffle2( self, argstr ):
    self.flushBuf()
    s2 = self.mp3shuffle( argstr )
    self.re.append( s2 )
    self.doCmdDone = True
    return ''


  # mp3shuffle()
  # If no "args", read all .mp3 files from current directory.
  # "args" may be a list of file names, one per line (like gallery).
  # "args" list may contain directories, in which case, it loads mp3's from directories.
  def mp3shuffle( self, argstr ):
    myAudioTxt = 'myAudioTxt'+str(self.id)
    myAudio = 'myAudio'+str(self.id)
    sl = 'sl'+str(self.id)
    si = 'si'+str(self.id)
    aud = 'aud'+str(self.id)
    self.id += 1
    # Formulate a list of files with alternative names.
    # Start by seeing if there is a list of names.
    args = argstr.strip().split('\n')
    s2 = ''
    if len(args) <= 0 or (len(args) == 1 and len(args[0]) == 0):
      # No arguments, so pick mp3s from the "current directory".
      args = ['.']
    # loop through args to formulate mp3s
    
    #print( 'mp3shuffle() ', args )    
    
    mp3s = []
    for arg in args:
      
      #print( 'mp3shuffle(2) ', '('+arg+')' )
      
      if arg[0] == '/':
        fn = self.rootDir + arg
      else:
        fn = self.path + '/' + arg
      fn = abspath( fn )
      if len(fn) >= len(self.rootDir):
        if os.path.isdir( fn ):
          # define a function to traverse a directory
          def traverseDir( fn ):
            files = listdir( fn )
            # Pick out the .mp3 and .m4a files.
            for f in files:
              # hidden files start with '.', skip them
              if len(f) > 0 and f[0] != '.':
                ty = ''
                i = f.rfind('.')
                if i != -1:
                  ty = f[i:].lower()
                if ty == '.mp3' or ty == '.m4a':
                  #mp3s.append( f )
                  mp3s.append( fn[len(self.rootDir):]+'/'+f )
                else:
                  # see if we have a directory
                  ff = fn+'/'+f
                  if os.path.isdir( ff ):
                    # recurse in to pick up sub-directories
                    
                    #print( 'mp3shuffle() recursing ', ff )
                    
                    traverseDir( ff )
                  else :
                  
                    pass
                    #print( 'mp3shuffle() skipped: ', f )
          
          # traverse the directory (and possibly recurse)          
          traverseDir( fn )
        elif os.path.isfile( fn ) :
          # Must be a file
          # hidden files start with '.', skip them
          if len(fn) > 0 and fn[0] != '.':
            ty = ''
            i = fn.rfind('.')
            if i != -1:
              ty = fn[i:].lower()
            if ty == '.mp3' or ty == '.m4a':
      
              #print( 'mp3shuffle(2.5) ', fn, fn[len(self.rootDir):] )
      
              mp3s.append(fn[len(self.rootDir):])
            else:
              
              print( 'mp3shuffle() skipped: ', f )
              
            #
          #
        else:
          
          print( 'mp3shuffle() not a file: ', fn )
          
        #
      #
    # (loop end)
    # Process mp3s
    if len(mp3s) > 0:
      
      #print( 'mp3shuffle(3) ', mp3s )
      
      # shuffle mp3s
      for ii in range(len(mp3s)):
        jj = random.randint(0,len(mp3s)-1)
        t = mp3s[ii]
        mp3s[ii] = mp3s[jj]
        mp3s[jj] = t
      # Do the HTML
      s2 = ( 
        '<div id="'+myAudioTxt+'">' + mp3s[0] + '</div>\n'
        '<audio id="'+myAudio+'" controls><source src="' +
        mp3s[0] + '" type="audio/mpeg"></audio>\n'
        '<script>\n'
        '  const '+sl+' = [\n' 
        )
      for i in range(len(mp3s)):
        s = '    "'+mp3s[i]+'"'
        if i < (len(mp3s)-1): s += ',\n'
        else: s += '];\n'
        s2 += s
      s2 += (
        '  let '+si+' = 0;\n'
        '  let '+aud+' = document.getElementById("'+myAudio+'");\n'
        '  '+aud+'.onended = function() {\n'
        '    '+si+'++;\n'
        '    if ('+si+' >= '+sl+'.length) '+si+' = 0;\n'
        '    let src = '+sl+'['+si+'];\n'
        '    this.src = src;\n'
        '    this.innerHTML = '+"'<source src="+'"'+"'+src+'"+'" type="audio/mpeg">'+"';\n"
        '    this.play();\n'
        '    // Show what song we are playing\n'
        '    document.getElementById("'+myAudioTxt+'").innerHTML = src;\n'
        '  };\n'
        '</script>\n' )
      #
    #
    return s2

  '''
  # http2()
  def http2( self, argstr ):
    self.flushBuf()
    s2 = self.http( argstr )
    self.re.append( s2 )
    self.doCmdDone = True
    return ''
  '''

  #define http2()
  def http2( self, argstr ):
    html = '<i><strike>' + argstr + '</strike></i>\n'
    args = argstr.split('|')
    if len(args) > 0:
      if len(args) >= 2:
        html = '<a href="' + args[0] + '" target="_blank">' + args[1] + '</a>'
      else:
        html = '<a href="' + args[0] + '" target="_blank">' + args[0] + '</a>'
    
    #print( self.pbuf + html )
    
    #self.re.append( self.pbuf + html )
    self.doCmdDone = True
    return html
    #return ''


  #define http()
  def http( self, argstr ):
    html = '<i><strike>' + argstr + '</strike></i>\n'
    args = argstr.split('|')
    if len(args) > 0:
      if len(args) >= 2:
        html = '<a href="' + args[0] + '" target="_blank">' + args[1] + '</a>'
      else:
        html = '<a href="' + args[0] + '" target="_blank">' + args[0] + '</a>'
    return html


  # localFile()
  def localFile( self, argstr ):
    html = '<i><strike>' + argstr + '</strike></i>\n'
    args = argstr.split('|')
    if len(args) > 0:
      for i in range(0,len(args)): args[i] = args[i].strip()
      # Save the old link name in the form the user will recognize
      if len(args) < 2: args.append(args[0])
      if len(args[0]) > 0 and args[0][0] == '/':
        # Change from absolute ("/file") to relative ("../../file") notation
        '''
        aa = self.path[len(self.rootDir):].split('/')
        if len(aa) <= 1: fix = "."
        elif len(aa) == 2: fix = ".."
        else:
          fix = ".."
          for j in range(len(aa)-2):
            fix += "/.."
        #
        '''
        args[0] = self.relPrefix() + args[0]
        fn = self.path + '/' + args[0]
      else:
        fn = self.path + '/' + args[0]
      if not (os.path.isfile( fn ) or os.path.isdir( fn )) :
        # not a file, search for file based on key words
        switched = False
        dir2 = ''
        sur = ''
        given = ''
        matches = []
        a = []
        for s in args[0].split(','):
          s = s.strip()
          if s != '': a.append( s )
        # need at least a given and surname for this search
        if len(a) > 1:
          while True:
            sur = a[0]
            given = a[1]
            word = []
            for i in range(1,len(a)):
              for s in a[i].split(' '):
                s = s.strip()
                if len(s) > 0: word.append( s.lower() )
            fn = '/' + self.site + '/' + sur.lower()
            dn = self.rootDir + fn
            if isdir( dn ):
              # look for qualifiers in directory names
              dirs = [f for f in listdir(dn) if isdir(join(dn, f))]
              for s in dirs:
                found = True
                for s2 in word:
                  if s2 not in s:
                    found = False
                    break
                if found: matches.append( s )
              if len(matches) > 0: dir2 = fn + '/' + matches[0] + '/'
            if len(dir2) > 0 or switched:
              break
            s = a[0]
            a[0] = a[1]
            a[1] = s
            switched = True
          #
        elif len(a) > 0:
          # see if it is a directory name
          dn = self.path + '/' + a[0]
          if isdir( dn ): 
            dir2 = dn[len(self.rootDir):] + '/'
          
            #print( 'dn upper: ' + dn )
          
          else:
            # check lower case directory name
            dn = self.path + '/' + a[0].lower()
            if isdir( dn ):
              dir2 = dn[len(self.rootDir):] + '/'
          
              #print( 'dn lower: ', dn, dir2 )
              
            #else:
            #  print( 'dn not found: ' + dn )

        if len(dir2) > 0:
          # if there is space, have it display the original name
          if len(args) < 2: args.append(args[0])
          # Change from absolute ("/file") to relative ("../../file") notation
          dir2 = self.relPrefix() + dir2
          # also, check to see if there is an .html file
          html = '<a href="' + self.maybeHtml(dir2) + '">'
          if len(args) > 1:
            html += args[1] + '</a>'
          else:
            if switched:
              # it is a person or couple
              if given != '':
                html += given + ' ' + sur
                if len(a) > 2:
                  for i in range(2,len(a)):
                    if i == 2: html += ' (' + a[i]
                    else: html += ', ' + a[i]
                  html += ')'
              else:
                html += sur
            else:
              # it is a place or something
              html += argstr.strip('[]')
            html += '</a>'
          # if duplicate detected, show there was a duplicate
          if len(matches) > 1:
            html += '<i><strike> (dup: ' + matches[1] + ')</strike></i>\n'
        else:
          html = '<i><strike>' + argstr.strip('[]') + '</strike></i>\n'
      else:
        # it is a file or a directory
        f3 = args[0]
        
        #print( 'fn, args[0]:',fn,args[0] )
        
        if not self.isRealTime:
          # if it is a ".txt" or ".ww" file and there is a ".html" file by 
          # the same name, then change the file name to ".html".
          #
          # If it is a directory, we need to see if there is an index.html
          # file in it.
          if os.path.isdir( fn ):
            f2 = fn + '/index.html'
            if os.path.isfile( f2 ):
              fn = f2
              args[0] = args[0] + '/index.html'
          #
          f3 = args[0]
          ty = ''
          i = fn.rfind('.')
          if i != -1:
            ty = fn[i:].lower()
          if ty == '.txt' or ty == '.ww':
            # check to see if there is an .html file
            f2 = fn[:len(fn)-len(ty)] + '.html'
            if( os.path.isfile(f2) ):
              # use '.html' version
              f3 = f2[len(self.path)+1:]
            #
          #
        # 
        if len(args) >= 2:
          html = '<a href="' + f3 + '">' + args[1] + '</a>'
        else:
          html = '<a href="' + f3 + '">' + args[0] + '</a>'
        #
      #
    #
    self.doCmdDone = True
    return html

