#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090308
# eLyXer main script


import sys
import codecs
#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090131
# eLyXer trace library

import sys

class Trace(object):
  "A tracing class"

  debugmode = False
  quietmode = False
  showlinesmode = False

  prefix = None

  def debug(cls, message):
    "Show a debug message"
    if Trace.debugmode and not Trace.quietmode:
      print message

  def message(cls, message):
    "Show a trace message"
    if Trace.quietmode:
      return
    if Trace.prefix and Trace.showlinesmode:
      message = Trace.prefix + message
    print message

  def error(cls, message):
    "Show an error message"
    if Trace.prefix and Trace.showlinesmode:
      message = Trace.prefix + message
    message = message.encode('utf-8')
    sys.stderr.write(message + '\n')

  debug = classmethod(debug)
  message = classmethod(message)
  error = classmethod(error)

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090308
# File line management for eLyXer

import sys


class LineReader(object):
  "Reads a file line by line"

  def __init__(self, file):
    self.file = file
    self.linenumber = 0
    self.current = None
    self.split = None

  def currentline(self):
    "Get the current line"
    if not self.current:
      self.readline()
    return self.current

  def currentnonblank(self):
    "Get the current nonblank line"
    while (self.currentline() == '\n'):
      self.nextline()
    return self.currentline()

  def currentsplit(self):
    "Get the current nonblank line, split into words"
    if not self.split:
      self.split = self.currentnonblank().split()
    return self.split

  def nextline(self):
    "Go to next line"
    self.current = None
    self.split = None

  def readline(self):
    "Read a line from file"
    self.current = self.file.readline()
    if self.file == sys.stdin:
      self.current = self.current.decode('utf-8')
    self.linenumber += 1
    Trace.prefix = 'Line ' + str(self.linenumber) + ': '
    if self.linenumber % 1000 == 0:
      Trace.message('Parsing')

  def finished(self):
    "Have we finished reading the file"
    if len(self.currentline()) == 0:
      return True
    return False

  def close(self):
    self.file.close()

class HtmlWriter(object):
  "Writes an HTML file as a series of lists"

  def __init__(self, file):
    self.file = file

  def write(self, html):
    "Write a list of lines"
    for line in html:
      self.writeline(line)

  def writeline(self, line):
    "Write a line to file"
    if self.file == sys.stdout:
      line = line.encode('utf-8')
    self.file.write(line)

  def close(self):
    self.file.close()

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090311
# LyX styles in containers

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090203
# eLyXer parsers

import codecs
#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090313
# eLyXer runtime options

import codecs
#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090416
# Command line option parser

import codecs


class CommandLineParser(object):
  "A parser for runtime options"

  def __init__(self, options):
    self.options = options

  def parseoptions(self, args):
    "Parse command line options"
    while args[0].startswith('--'):
      if args[0] == '--help':
        return 'Help'
      key, value = self.readoption(args)
      if not key:
        return 'Option ' + original + ' not recognized'
      if not value:
        return 'Option ' + key + ' needs a value'
      setattr(self.options, key, value)
    return None

  def readoption(self, args):
    "Read the key and value for an option"
    arg = args[0][2:]
    del args[0]
    if '=' in arg:
      return self.readequals(arg, args)
    key = arg
    if not hasattr(self.options, key):
      return None, None
    current = getattr(self.options, key)
    if current.__class__ == bool:
      return key, True
    # read value
    if len(args) == 0:
      return key, None
    if args[0].startswith('"'):
      initial = args[0]
      del args[0]
      return key, self.readquoted(args, initial)
    value = args[0]
    del args[0]
    return key, value

  def readquoted(self, args, initial):
    "Read a value between quotes"
    value = initial[1:]
    while len(args) > 0 and not args[0].endswith('"') and not args[0].startswith('--'):
      value += ' ' + args[0]
      del args[0]
    if len(args) == 0 or args[0].startswith('--'):
      return None
    value += ' ' + args[0:-1]
    return value

  def readequals(self, arg, args):
    "Read a value with equals"
    split = arg.split('=', 1)
    key = split[0]
    value = split[1]
    if not value.startswith('"'):
      return key, value
    return key, self.readquoted(args, value)



class Options(object):
  "A set of runtime options"

  instance = None

  nocopy = False
  debug = False
  quiet = False
  showlines = True
  css = 'http://www.nongnu.org/elyxer/lyx.css'
  title = 'Converted document'
  directory = '.'
  branches = dict()

  def parseoptions(self, args):
    "Parse command line options"
    parser = CommandLineParser(Options)
    result = parser.parseoptions(args)
    # set in Trace if necessary
    for param in dir(Options):
      if hasattr(Trace, param + 'mode'):
        setattr(Trace, param + 'mode', getattr(self, param))
    return result

class BranchOptions(object):
  "A set of options for a branch"

  def __init__(self):
    self.selected = 0
    self.color = '#ffffff'

  def set(self, key, value):
    "Set a branch option"
    if not key.startswith('\\'):
      Trace.error('Invalid branch option ' + key)
      return
    key = key.replace('\\', '')
    setattr(self, key, value)



class ParseTree(object):
  "A parsing tree"

  default = '~~default~~'

  def __init__(self, types):
    "Create the parse tree"
    self.root = dict()
    for type in types:
      if hasattr(type, 'start'):
        self.addstart(type, type.start)
      elif hasattr(type, 'starts'):
        for start in type.starts:
          self.addstart(type, start)

  def addstart(self, type, start):
    "Add a start piece to the tree"
    tree = self.root
    for piece in start.split():
      if not piece in tree:
        tree[piece] = dict()
      tree = tree[piece]
    if ParseTree.default in tree:
      Trace.error('Start ' + start + ' duplicated')
    tree[ParseTree.default] = type

  def find(self, reader):
    "Find the current sentence in the tree"
    branches = [self.root]
    for piece in reader.currentsplit():
      current = branches[-1]
      piece = piece.rstrip('>')
      if piece in current:
        branches.append(current[piece])
    while not ParseTree.default in branches[-1]:
      Trace.error('Line ' + reader.currentline().strip() + ' not found')
      branches.pop()
    last = branches[-1]
    return last[ParseTree.default]

class Parser(object):
  "A generic parser"

  def __init__(self):
    self.begin = 0
    self.parameters = dict()

  def parseheader(self, reader):
    "Parse the header"
    header = reader.currentsplit()
    reader.nextline()
    self.begin = reader.linenumber
    return header

  def parseparameter(self, reader):
    "Parse a parameter"
    if reader.currentline().strip().startswith('<'):
      self.parsexml(reader)
      return
    split = reader.currentline().strip().split(' ', 1)
    reader.nextline()
    if len(split) == 0:
      return
    key = split[0]
    if len(split) == 1:
      self.parameters[key] = True
      return
    if not '"' in split[1]:
      self.parameters[key] = split[1].strip()
      return
    doublesplit = split[1].split('"')
    self.parameters[key] = doublesplit[1]

  def parsexml(self, reader):
    "Parse a parameter in xml form: <param attr1=value...>"
    strip = reader.currentline().strip()
    reader.nextline()
    if not strip.endswith('>'):
      Trace.error('XML parameter ' + strip + ' should be <...>')
    split = strip[1:-1].split()
    if len(split) == 0:
      return
    key = split[0]
    del split[0]
    if len(split) == 0:
      self.parameters[key] = dict()
      return
    attrs = dict()
    for attr in split:
      if not '=' in attr:
        Trace.error('Erroneous attribute ' + attr)
        return
      parts = attr.split('=')
      attrkey = parts[0]
      value = parts[1].split('"')[1]
      attrs[attrkey] = value
    self.parameters[key] = attrs

  def __str__(self):
    "Return a description"
    return self.__class__.__name__ + ' (' + str(self.begin) + ')'

class LoneCommand(Parser):
  "A parser for just one command line"

  def parse(self,reader):
    "Read nothing"
    return []

class TextParser(Parser):
  "A parser for a command and a bit of text"

  endings = ['\\end_layout', '\\end_inset', '\\emph', '\\family', '\\noun',
      '\\color', '\\size', '\\series']

  def parse(self, reader):
    "Parse lines as long as they are text"
    contents = []
    while not self.isending(reader):
      container = self.factory.create(reader)
      contents.append(container)
    return contents

  def isending(self, reader):
    "Check if text is ending"
    current = reader.currentsplit()
    if len(current) == 0:
      return True
    return current[0] in TextParser.endings

class ExcludingParser(Parser):
  "A parser that excludes the final line"

  def parse(self, reader):
    "Parse everything up to (and excluding) the final line"
    contents = []
    while not reader.currentnonblank().startswith(self.ending):
      container = self.factory.create(reader)
      contents.append(container)
    return contents

class BoundedParser(ExcludingParser):
  "A parser bound by a final line"

  def parse(self, reader):
    "Parse everything, including the final line"
    contents = ExcludingParser.parse(self, reader)
    # skip last line
    reader.nextline()
    return contents

class BoundedDummy(Parser):
  "A bound parser that ignores everything"

  def parse(self, reader):
    "Parse the contents of the container"
    while not reader.currentline().startswith(self.ending):
      reader.nextline()
    # skip last line
    reader.nextline()
    return []

class StringParser(Parser):
  "Parses just a string"

  def parseheader(self, reader):
    "Do nothing, just take note"
    self.begin = reader.linenumber + 1
    return []

  def parse(self, reader):
    "Parse a single line"
    contents = [reader.currentline()]
    reader.nextline()
    return contents

class InsetParser(BoundedParser):
  "Parses a LyX inset"

  def parse(self, reader):
    "Parse inset parameters into a dictionary"
    while reader.currentline() != '\n' and not reader.currentline().startswith('\\'):
      self.parseparameter(reader)
    return BoundedParser.parse(self, reader)

class HeaderParser(Parser):
  "Parses the LyX header"

  def parse(self, reader):
    "Parse header parameters into a dictionary"
    while not reader.currentline().startswith(self.ending):
      self.parseline(reader)
      reader.nextline()
    # skip last line
    reader.nextline()
    return []

  def parseline(self, reader):
    "Parse a single line as a parameter or as a start"
    line = reader.currentline()
    for key in HeaderParser.openings:
      if line.startswith(key):
        HeaderParser.openings[key](self, reader)
        return
    # no match
    self.parseparameter(reader)

  def parsebranch(self, reader):
    branch = reader.currentline().split()[1]
    reader.nextline()
    subparser = HeaderParser().complete('\\end_branch')
    subparser.parse(reader)
    options = BranchOptions()
    for key in subparser.parameters:
      options.set(key, subparser.parameters[key])
    Options.branches[branch] = options

  def complete(self, ending):
    self.ending = ending
    return self

  openings = {'\\branch':parsebranch}

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090203
# eLyXer html outputters

import codecs
import datetime


class EmptyOutput(object):
  "The output for some container"

  def gethtml(self, container):
    "Return empty HTML code"
    return []

class FixedOutput(object):
  "Fixed output"

  def gethtml(self, container):
    "Return constant HTML code"
    return container.html

class ContentsOutput(object):
  "Outputs the contents converted to HTML"

  def gethtml(self, container):
    "Return the HTML code"
    html = []
    if container.contents == None:
      return html
    for element in container.contents:
      if not hasattr(element, 'gethtml'):
        Trace.error('No html in ' + unicode(element))
        return html
      html += element.gethtml()
    return html

class TaggedOutput(ContentsOutput):
  "Outputs an HTML tag surrounding the contents"

  def __init__(self):
    self.breaklines = False

  def settag(self, tag, breaklines=False):
    "Set the value for the tag"
    self.tag = tag
    self.breaklines = breaklines
    return self

  def setbreaklines(self, breaklines):
    "Set the value for breaklines"
    self.breaklines = breaklines
    return self

  def gethtml(self, container):
    "Return the HTML code"
    if hasattr(container, 'breaklines'):
      self.breaklines = container.breaklines
    if hasattr(container, 'tag'):
      self.tag = container.tag
    html = [self.getopen(container)]
    html += ContentsOutput.gethtml(self, container)
    html.append(self.getclose(container))
    return html

  def getopen(self, container):
    "Get opening line"
    if self.tag == '':
      return ''
    open = '<' + self.tag + '>'
    if self.breaklines:
      return open + '\n'
    return open

  def getclose(self, container):
    "Get closing line"
    if self.tag == '':
      return ''
    close = '</' + self.tag.split()[0] + '>'
    if self.breaklines:
      return '\n' + close + '\n'
    return close

class MirrorOutput(object):
  "Returns as output whatever comes along"

  def gethtml(self, container):
    "Return what is put in"
    return container.contents

class HeaderOutput(object):
  "Returns the HTML headers"

  def gethtml(self, container):
    "Return a constant header"
    html = [u'<?xml version="1.0" encoding="UTF-8"?>\n']
    html.append(u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
    html.append(u'<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">\n')
    html.append(u'<head>\n')
    html.append(u'<meta name="generator" content="http://www.nongnu.org/elyxer/"/>\n')
    html.append(u'<meta name="create-date" content="' + datetime.date.today().isoformat() + '"/>\n')
    html.append(u'<link rel="stylesheet" href="' + Options.css + '" type="text/css" media="screen"/>\n')
    html.append(u'<title>' + Options.title + '</title>\n')
    html.append('</head>\n')
    html.append('<body>\n')
    html.append('<div id="globalWrapper">\n')
    return html

class FooterOutput(object):
  "Return the HTML code for the footer"

  author = None

  def gethtml(self, container):
    "Footer HTML"
    html = []
    if FooterOutput.author and not Options.nocopy:
      html.append('<hr/>\n')
      year = datetime.date.today().year
      html.append('<p>Copyright (C) ' + str(year) + ' ' + FooterOutput.author
          + '</p>\n')
    html.append('</div>\n')
    html.append('</body>\n')
    html.append('</html>\n')
    return html

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090131
# eLyXer containers for Lyx data that output HTML

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090320
# eLyXer general configuration


class FormulaConfig(object):
  "Configuration for formulae"

  unmodified = ['.', '*', u'€', '(', ')', '[', ']', ':', u'·', '!', ';']
  modified = {'\'':u'’', '=':u' = ', ' ':'', '<':u' &lt; ', '>':u' &gt; ',
      '-':u' − ', '+':u' + ',
      ',':u', ', '/':u' ⁄ ', '\n':'', '&':u'\t'}
  commands = {
      # mathematical
      '\\times':u' × ', '\\propto':u' ∝ ', '\\cdot':u'⋅', '\\approx':u' ≈ ',
      '\\pm':u'±', '\\sim':u' ~ ', '\\implies':u'  ⇒  ', '\\not':u'¬',
      '\\sum':u'<span class="bigsymbol">∑</span>',
      '\\int':u'<span class="bigsymbol">∫</span>',
      '\\intop':u'<span class="bigsymbol">∫</span>', '\\infty':u'∞',
      '\\prime':u'′', '\\ddots':u'⋱',
      '\\leq':u'≤', '\\geq':u'≥', '\\neq':u'≠', '\\in':u'∈',
      # symbols
      '\\rightarrow':u' → ', '\\rightsquigarrow':u' ⇝ ', '\\Rightarrow':u' ⇒ ',
      '\\leftarrow':u' ← ',
      '\\dashrightarrow':u' ⇢ ', '\\blacktriangleright':u'▶', '\\bullet':u'•',
      '\\dagger':u'†', '\\ddagger':u'‡', '\\bigstar':u'★',
      '\\to':u'→', '\\gets':u'←', '\\circ':u'○', '\\diamond':u'◇',
      '\\triangleright':u'▷',
      # common functions
      '\\log':'log', '\\exp':'exp', '\\ln':'ln', '\\lim':'lim', '\\sin':'sin',
      '\\cos':'cos',
      # hyperbolic functions
      '\\tanh':'tanh', '\\sinh':'sinh', '\\cosh':'cosh',
      # LaTeX (ignored)
      '\\nonumber':'', '\\lyxlock':'', '\\end{array}':'',
      '\\displaystyle':'', '\\textstyle':'', '\\scriptstyle':'',
      '\\scriptscriptstyle':'',
      # spacing
      '\\,':' ', '\\\\':'<br/>', '\\quad':u' ', '\\!':'', '\\:':u' ',
      # typographical
      '\\%':'%', '\\_':'_',
      '\\left(':u'<span class="bigsymbol">(</span>',
      '\\right)':u'<span class="bigsymbol">)</span>',
      '\\left[':u'<span class="bigsymbol">[</span>',
      '\\right]':u'<span class="bigsymbol">]</span>',
      }
  alphacommands = {
      # regional
      '\\tilde{n}':u'ñ', '\\tilde{N}':u'Ñ',
      '\\acute{a}':u'á', '\\acute{e}':u'é', '\\acute{i}':u'í',
      '\\acute{o}':u'ó', '\\acute{u}':u'ú',
      '\\acute{A}':u'Á', '\\acute{E}':u'É', '\\acute{I}':u'Í',
      '\\acute{O}':u'Ó', '\\acute{U}':u'Ú',
      # greek
      '\\alpha':u'α', '\\beta':u'β', '\\gamma':u'γ', '\\delta':u'δ',
      '\\epsilon':u'ε', '\\lambda':u'λ', '\\Delta':u'Δ',
      '\\sigma':u'σ', '\\pi':u'π',
      }
  onefunctions = {
      # typographical
      '^':'sup', '_':'sub', '\\underline':'u', '\\overline':'span class="overline"',
      '\\bar':'span class="bar"', '\\mbox':'span class="mbox"',
      # functions
      '\\sqrt':'span class="sqrt"',
      # hard functions
      '\\begin{array}':'span class="arraydef"',
      # LaTeX (ignored)
      '\\label':''
      }
  fontfunctions = {
      '\\mathsf':'span class="mathsf"', '\\mathbf':'b',
      '\\textrm':'span class="mathrm"', '\\mathrm':'span class="mathrm"',
      '\\text':'span class="text"', '\\textipa':'span class="textipa"',
      '\\boldsymbol':'b', '\\mathit':'i', '\\mathtt':'tt',
      '\\mathbb':'span class="blackboard"',
      '\\mathfrak':'span class="fraktur"', '\\mathcal':'span class="script"',
      }
  decoratingfunctions = {
      # symbols above (start with *)
      '\\check':u'ˇ', '\\breve':u'˘', '\\vec':u'→', '\\dot':u'˙',
      '\\hat':u'^', '\\grave':u'`', '\\ddot':u'¨', '\\tilde':u'˜',
      '\\acute':u'´'
      }
  twofunctions = {
      '\\frac':['span class="fraction"', 'span class="numerator"', 'span class="denominator"'],
      '\\nicefrac':['span class="fraction"', 'span class="numerator"', 'span class="denominator"']
      }

class ContainerConfig(object):
  "Low-level configuration for containers"

  escapes = {'&':'&amp;', '<':'&lt;', '>':'&gt;'}
  replaces = { '`':u'‘', '\'':u'’', '\n':'', ' -- ':u' — ' }
  commands = { '\\SpecialChar \\ldots{}':u'…', '\\InsetSpace ~':'&nbsp;',
      '\\InsetSpace \\space{}':'&nbsp;', '\\InsetSpace \\thinspace{}':u' ',
      '\\backslash':'\\', '\\SpecialChar \\@.':'.',
      '\\SpecialChar \\menuseparator':u'&nbsp;▷&nbsp;',
      '\\SpecialChar \\textcompwordmark{}':u'', '\\SpecialChar \\nobreakdash-':'-',
      '\\SpecialChar \\slash{}':'/'}

class BlackBoxConfig(object):
  "Configuration for lines ignored"

  starts = ['\\lyxformat', '\\begin_document', '\\begin_body',
      '\\family default', '\\color inherit',
      '\\shape default', '\\series default', '\\emph off',
      '\\bar no', '\\noun off', '\\emph default', '\\bar default',
      '\\noun default', '\\family roman', '\\series medium',
      '\\shape up', '\\size normal', '\\color none', '#LyX', '\\noindent',
      '\\labelwidthstring', '\\paragraph_spacing', '\\length']

class SpaceConfig(object):
  "Configuration for spaces"

  spaces = {
      '~':'&nbsp;', '\\space{}':'&nbsp;', '\\thinspace{}':u' ',
      '\\hfill{}':u' ', '\\hspace*{\\fill}':u' ', '\\hspace{}':u' ',
      '\\negthinspace{}':u'', '\\enskip{}':u' ', '\\quad{}': u' ',
      '\\qquad{}':u'  '
      }

class TranslationConfig(object):
  "Configuration for messages to translate"

  floats = {
      'figure':'Figure ', 'algorithm':'Listing ', 'table':'Table '
      }



class Container(object):
  "A container for text and objects in a lyx file"

  def __init__(self):
    self.contents = list()

  def comesnext(cls, reader):
    "Return if the current line matches"
    line = reader.currentline()
    if hasattr(cls, 'start'):
      return line.startswith(cls.start)
    if hasattr(cls, 'starts'):
      for start in cls.starts:
        if line.startswith(start):
          return True
    return False

  comesnext = classmethod(comesnext)

  def parse(self, reader):
    "Parse by lines"
    if hasattr(self, 'ending'):
      self.parser.ending = self.ending
    self.parser.factory = self.factory
    self.header = self.parser.parseheader(reader)
    self.begin = self.parser.begin
    self.contents = self.parser.parse(reader)
    self.parameters = self.parser.parameters
    self.process()
    self.parser = None

  def process(self):
    "Process contents"
    pass

  def finished(self, reader):
    "Find out if we are at the end"
    return reader.currentline().startswith(self.ending)

  def gethtml(self):
    "Get the resulting HTML"
    return self.output.gethtml(self)

  def __str__(self):
    "Get a description"
    if not hasattr(self, 'begin'):
      return self.__class__.__name__
    return self.__class__.__name__ + '@' + str(self.begin)

  def escape(self, line, escapes = ContainerConfig.escapes):
    "Escape a line to appear in HTML"
    pieces = escapes.keys()
    # do the '&' first
    pieces.sort()
    for piece in pieces:
      if piece in line:
        line = line.replace(piece, escapes[piece])
    return line

  def searchfor(self, type):
    "Search for an embedded container of a given type recursively"
    for element in self.contents:
      if isinstance(element, Container):
        if isinstance(element, type):
          return element
        result = element.searchfor(type)
        if result:
          return result
    return None

  def searchall(self, type):
    "Search for all embedded containers of a given type"
    list = []
    for element in self.contents:
      if isinstance(element, Container):
        if isinstance(element, type):
          list.append(element)
        list += element.searchall(type)
    return list

  def restyle(self, type, restyler):
    "Restyle contents with a restyler function"
    for index, element in enumerate(self.contents):
      if isinstance(element, type):
        restyler(self, index)
      if isinstance(element, Container):
        element.restyle(type, restyler)

  def group(self, index, group, isingroup):
    "Group some adjoining elements into a group"
    if index >= len(self.contents):
      return
    if hasattr(self.contents[index], 'grouped'):
      return
    while index < len(self.contents) and isingroup(self.contents[index]):
      self.contents[index].grouped = True
      group.contents.append(self.contents[index])
      self.contents.pop(index)
    self.contents.insert(index, group)

  def remove(self, index):
    "Remove a container but leave its contents"
    container = self.contents[index]
    self.contents.pop(index)
    while len(container.contents) > 0:
      self.contents.insert(index, container.contents.pop())

class BlackBox(Container):
  "A container that does not output anything"

  starts = BlackBoxConfig.starts

  def __init__(self):
    self.parser = LoneCommand()
    self.output = EmptyOutput()
    self.contents = []

class StringContainer(Container):
  "A container for a single string"

  start = ''

  def __init__(self):
    self.parser = StringParser()
    self.output = MirrorOutput()

  def process(self):
    "Replace special chars"
    line = self.contents[0]
    replaced = self.escape(line)
    replaced = self.changeline(replaced)
    self.contents = [replaced]
    if '\\' in replaced and len(replaced) > 1:
      # unprocessed commands
      Trace.error('Unknown command at ' + str(self.parser.begin) + ': '
          + replaced.strip())

  def changeline(self, line):
    line = self.replacemap(line, ContainerConfig.replaces)
    if not '\\' in line:
      return line
    line = self.replacemap(line, ContainerConfig.commands)
    return line

  def replacemap(self, line, map):
    for piece in map:
      if piece in line:
        line = line.replace(piece, map[piece])
    return line
  
  def __str__(self):
    length = ''
    descr = ''
    if len(self.contents) > 0:
      length = str(len(self.contents[0]))
      descr = self.contents[0].strip()
    return 'StringContainer@' + str(self.begin) + '(' + str(length) + ')'

class Constant(StringContainer):
  "A constant string"

  def __init__(self, text):
    self.contents = [text]
    self.output = MirrorOutput()

  def __str__(self):
    return 'Constant'

class TaggedText(Container):
  "Text inside a tag"

  def __init__(self):
    self.parser = TextParser()
    self.output = TaggedOutput()

  def complete(self, contents, tag, breaklines=False):
    "Complete the tagged text and return it"
    self.contents = contents
    self.tag = tag
    self.breaklines = breaklines
    return self

  def constant(self, text, tag, breaklines=False):
    "Complete the tagged text with a constant"
    constant = Constant(text)
    return self.complete([constant], tag, breaklines)

  def __str__(self):
    return 'Tagged <' + self.tag + '>'

class ContainerFactory(object):
  "Creates containers depending on the first line"

  types = [BlackBox, StringContainer]

  def __init__(self):
    self.tree = ParseTree(ContainerFactory.types)

  def create(self, reader):
    "Get the container and parse it"
    #Trace.debug('processing "' + reader.currentline() + '"')
    type = self.tree.find(reader)
    container = type.__new__(type)
    container.__init__()
    container.factory = self
    container.parse(reader)
    return container



class QuoteContainer(Container):
  "A container for a pretty quote"

  start = '\\begin_inset Quotes'
  ending = '\\end_inset'
  outputs = {
      'eld':u'“', 'erd':u'”', 'els':u'‘', 'ers':u'’',
      'sld':u'”', 'srd':u'”',
      'gld':u'„', 'grd':u'“', 'gls':u'‚', 'grs':u'‘',
      'pld':u'„', 'prd':u'”', 'pls':u'‚', 'prs':u'’',
      'fld':u'«', 'frd':u'»', 'fls':u'‹', 'frs':u'›',
      'ald':u'»', 'ard':u'«', 'als':u'›', 'ars':u'‹'
      }

  def __init__(self):
    self.parser = BoundedParser()
    self.output = FixedOutput()

  def process(self):
    "Process contents"
    self.type = self.header[2]
    if not self.type in QuoteContainer.outputs:
      Trace.error('Quote type ' + self.type + ' not found')
      self.html = '"'
      return
    self.html = QuoteContainer.outputs[self.type]

class LyxLine(Container):
  "A Lyx line"

  start = '\\lyxline'

  def __init__(self):
    self.parser = LoneCommand()
    self.output = FixedOutput()

  def process(self):
    self.html = ['<hr class="line" />']

class EmphaticText(TaggedText):
  "Text with emphatic mode"

  start = '\\emph on'

  def process(self):
    self.output.tag = 'i'

class ShapedText(TaggedText):
  "Text shaped (italic, slanted)"

  start = '\\shape'

  tags = {'slanted':'i', 'italic':'i', 'smallcaps':'span class="versalitas"'}

  def process(self):
    self.type = self.header[1]
    if not self.type in ShapedText.tags:
      Trace.error('Unrecognized shape ' + self.header[1])
      self.output.tag = 'span'
      return
    self.output.tag = ShapedText.tags[self.type]

class VersalitasText(TaggedText):
  "Text in versalitas"

  start = '\\noun on'

  def process(self):
    self.output.tag = 'span class="versalitas"'

class ColorText(TaggedText):
  "Colored text"

  start = '\\color'

  def process(self):
    self.color = self.header[1]
    self.output.tag = 'span class="' + self.color + '"'

class SizeText(TaggedText):
  "Sized text"

  start = '\\size'

  def process(self):
    self.size = self.header[1]
    self.output.tag = 'span class="' + self.size + '"'

class BoldText(TaggedText):
  "Bold text"

  start = '\\series bold'

  def process(self):
    self.output.tag = 'b'

class TextFamily(TaggedText):
  "A bit of text from a different family"

  start = '\\family'
  typetags = { 'typewriter':'tt', 'sans':'span class="sans"' }

  def process(self):
    "Parse the type of family"
    self.type = self.header[1]
    self.output.tag = TextFamily.typetags[self.type]

class Hfill(TaggedText):
  "Horizontall fill"

  start = '\\hfill'

  def process(self):
    self.output.tag = 'span class="right"'

class BarredText(TaggedText):
  "Text with a bar somewhere"

  start = '\\bar'
  typetags = { 'under':'u' }

  def process(self):
    "Parse the type of bar"
    self.type = self.header[1]
    if not self.type in BarredText.typetags:
      Trace.error('Unknown bar type ' + self.type)
      self.output.tag = 'span'
      return
    self.output.tag = BarredText.typetags[self.type]

class FlexCode(Container):
  "A bit of inset code"

  start = '\\begin_inset Flex CharStyle:Code'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('span class="code"', True)

class InfoInset(Container):
  "A LyX Info inset"

  start = '\\begin_inset Info'
  ending = '\\end_inset'

  types = ['shortcut', 'shortcuts']

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('span class="Info"', False)

  def process(self):
    "Set the shortcut as text"
    self.type = self.parser.parameters['type']
    if self.type not in InfoInset.types:
      Trace.error('Unknown Info type ' + self.type)
    self.contents = [Constant(self.parser.parameters['arg'])]

class LangLine(Container):
  "A line with language information"

  start = '\\lang'

  def __init__(self):
    self.parser = LoneCommand()
    self.output = EmptyOutput()

  def process(self):
    self.lang = self.header[1]

ContainerFactory.types += [
    QuoteContainer, LyxLine, EmphaticText, ShapedText, VersalitasText,
    ColorText, SizeText, BoldText, TextFamily, Hfill, FlexCode, BarredText,
    InfoInset, LangLine
    ]

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090218
# eLyXer links

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090418
# eLyXer number generator



class NumberGenerator(object):
  "A number generator for unique sequences and hierarchical structures"

  letters = '-ABCDEFGHIJKLMNOPQRSTUVWXYZ'

  instance = None

  def __init__(self):
    self.startinglevel = 0
    self.number = []
    self.uniques = dict()
    self.chaptered = dict()

  def generateunique(self, type):
    "Generate a number to place in the title but not to append to others"
    if not type in self.uniques:
      self.uniques[type] = 0
    self.uniques[type] = self.increase(self.uniques[type])
    return type + ' ' + str(self.uniques[type]) + '.'

  def generate(self, level):
    "Generate a number in the given level"
    if self.number == [] and level == 1:
      self.startinglevel = 1
    level -= self.startinglevel
    if len(self.number) > level:
      self.number = self.number[:level + 1]
    else:
      while len(self.number) <= level:
        self.number.append(0)
    self.number[level] = self.increase(self.number[level])
    return self.dotseparated(self.number)

  def generatechaptered(self, type):
    "Generate a number which goes with first-level numbers"
    if len(self.number) == 0:
      chapter = 0
    else:
      chapter = self.number[0]
    if not type in self.chaptered or self.chaptered[type][0] != chapter:
      self.chaptered[type] = [chapter, 0]
    chaptered = self.chaptered[type]
    chaptered[1] = self.increase(chaptered[1])
    self.chaptered[type] = chaptered
    return self.dotseparated(chaptered)

  def increase(self, number):
    "Increase the number (or letter)"
    if not isinstance(number, str):
      return number + 1
    if not number in NumberGenerator.letters:
      Trace.error('Unknown letter numeration ' + number)
      return 0
    index = NumberGenerator.letters.index(number) + 1
    return NumberGenerator.letters[index % len(NumberGenerator.letters)]

  def dotseparated(self, number):
    "Get the number separated by dots: 1.1.3"
    dotsep = ''
    if len(number) == 0:
      Trace.error('Empty number')
      return '.'
    for piece in number:
      dotsep += '.' + str(piece)
    return dotsep[1:]

NumberGenerator.instance = NumberGenerator()



class Link(Container):
  "A link to another part of the document"

  def __init__(self):
    self.contents = list()
    self.output = LinkOutput()

  def complete(self, text, anchor, url, type = None):
    self.contents = [Constant(text)]
    if anchor:
      self.anchor = anchor
    if url:
      self.url = url
    if type:
      self.type = type
    return self

class ListOf(Container):
  "A list of entities (figures, tables, algorithms)"

  start = '\\begin_inset FloatList'
  ending = '\\end_inset'

  names = {'figure':'figures', 'table':'tables', 'algorithm':'listings'}

  def __init__(self):
    self.parser = BoundedParser()
    self.output = TaggedOutput().settag('div class="list"', True)

  def process(self):
    "Parse the header and get the type"
    self.type = self.header[2]
    self.contents = [Constant(u'List of ' + ListOf.names[self.type])]

class TableOfContents(Container):
  "Table of contents"

  starts = ['\\begin_inset LatexCommand tableofcontents',
      '\\begin_inset CommandInset toc']
  ending = '\\end_inset'

  def __init__(self):
    self.parser = BoundedParser()
    self.output = TaggedOutput().settag('div class="toc"', True)

  def process(self):
    "Parse the header and get the type"
    self.contents = [Constant(u'Table of Contents')]

class IndexEntry(Link):
  "An entry in the alphabetical index"

  start = '\\begin_inset LatexCommand index'
  ending = '\\end_inset'

  entries = dict()

  namescapes = {'!':'', '|':', ', '  ':' '}
  keyescapes = {' ':'-', '--':'-', ',':''}

  def __init__(self):
    self.parser = InsetParser()
    self.output = LinkOutput()

  def process(self):
    "Put entry in index"
    name = self.parser.parameters['name'].strip()
    self.name = self.escape(name, IndexEntry.namescapes)
    self.key = self.escape(self.name, IndexEntry.keyescapes)
    if not self.key in IndexEntry.entries:
      # no entry; create
      IndexEntry.entries[self.key] = list()
    self.index = len(IndexEntry.entries[self.key])
    IndexEntry.entries[self.key].append(self)
    self.anchor = 'entry-' + self.key + '-' + str(self.index)
    self.url = '#index-' + self.key
    self.contents = [Constant(u'↓')]

class LayoutIndexEntry(IndexEntry):
  "An entry with the name in a layout"

  start = '\\begin_inset Index'
  ending = '\\end_inset'

  def process(self):
    "Read entry from layout and put in index"
    name = ''
    layout = self.contents[0]
    for element in layout.contents:
      if isinstance(element, StringContainer):
        name += element.contents[0]
      else:
        name += ' '
    self.parser.parameters['name'] = name
    IndexEntry.process(self)

class PrintIndex(Container):
  "Command to print an index"

  starts = ['\\begin_inset LatexCommand printindex',
      '\\begin_inset CommandInset index_print']
  ending = '\\end_inset'

  def __init__(self):
    self.parser = BoundedParser()
    self.output = ContentsOutput()

  def process(self):
    "Create the alphabetic index"
    self.contents = [TaggedText().constant('Index', 'h1 class="index"'),
        Constant('\n')]
    for key in self.sortentries():
      name = IndexEntry.entries[key][0].name
      entry = [Link().complete(name, 'index-' + key, None, 'printindex'),
          Constant(': ')]
      contents = [TaggedText().complete(entry, 'i')]
      contents += self.createarrows(key, IndexEntry.entries[key])
      self.contents.append(TaggedText().complete(contents, 'p class="printindex"',
          True))

  def sortentries(self):
    "Sort all entries in the index"
    keys = IndexEntry.entries.keys()
    # sort by name
    keys.sort()
    return keys

  def createarrows(self, key, entries):
    "Create an entry in the index"
    arrows = []
    for entry in entries:
      link = Link().complete(u'↑', 'index-' + entry.key,
          '#entry-' + entry.key + '-' + str(entry.index))
      arrows += [link, Constant(u', \n')]
    return arrows[:-1]

class NomenclatureEntry(Link):
  "An entry of LyX nomenclature"

  start = '\\begin_inset CommandInset nomenclature'
  ending = '\\end_inset'

  entries = {}

  def __init__(self):
    self.parser = InsetParser()
    self.output = LinkOutput()

  def process(self):
    "Put entry in index"
    self.symbol = self.parser.parameters['symbol']
    self.description = self.parser.parameters['description']
    self.key = self.symbol.replace(' ', '-').lower()
    NomenclatureEntry.entries[self.key] = self
    self.anchor = 'noment-' + self.key
    self.url = '#nom-' + self.key
    self.contents = [Constant(u'↓')]

class NomenclaturePrint(Container):
  "Print all nomenclature entries"

  start = '\\begin_inset CommandInset nomencl_print'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = ContentsOutput()

  def process(self):
    self.keys = self.sortentries()
    self.contents = [TaggedText().constant('Nomenclature', 'h1 class="nomenclature"')]
    for key in self.keys:
      entry = NomenclatureEntry.entries[key]
      contents = [Link().complete(u'↑', 'nom-' + key, '#noment-' + key)]
      contents.append(Constant(entry.symbol + u' '))
      contents.append(Constant(entry.description))
      text = TaggedText().complete(contents, 'div class="Nomenclated"', True)
      self.contents.append(text)

  def sortentries(self):
    "Sort all entries in the index"
    keys = NomenclatureEntry.entries.keys()
    # sort by name
    keys.sort()
    return keys

class URL(Link):
  "A clickable URL"

  starts = ['\\begin_inset LatexCommand url',
      '\\begin_inset LatexCommand htmlurl', '\\begin_inset CommandInset href']
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = LinkOutput()

  def process(self):
    "Read URL from parameters"
    self.url = self.escape(self.parser.parameters['target'])
    if 'type' in self.parser.parameters:
      self.url = self.escape(self.parser.parameters['type']) + self.url
    name = self.url
    if 'name' in self.parser.parameters:
      name = self.parser.parameters['name']
    self.contents = [Constant(name)]

class FlexURL(URL):
  "A flexible URL"

  start = '\\begin_inset Flex URL'
  ending = '\\end_inset'

  def process(self):
    "Read URL from contents"
    text = self.searchfor(StringContainer).contents[0]
    self.url = self.escape(text)
    self.contents = [Constant(self.url)]

class LinkOutput(object):
  "A link pointing to some destination"
  "Or an anchor (destination)"

  def gethtml(self, container):
    "Get the HTML code for the link"
    type = container.__class__.__name__
    if hasattr(container, 'type'):
      type = container.type
    tag = 'a class="' + type + '"'
    if hasattr(container, 'anchor'):
      tag += ' name="' + container.anchor + '"'
    if hasattr(container, 'url'):
      tag += ' href="' + container.url + '"'
    text = TaggedText().complete(container.contents, tag)
    return text.gethtml()

ContainerFactory.types += [
    ListOf, TableOfContents, IndexEntry, PrintIndex, URL,
    FlexURL, NomenclatureEntry, NomenclaturePrint, LayoutIndexEntry
    ]

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090218
# eLyXer labels



class Label(Container):
  "A label to be referenced"

  starts = ['\\begin_inset LatexCommand label', '\\begin_inset CommandInset label']
  ending = '\\end_inset'

  names = dict()

  def __init__(self):
    self.parser = InsetParser()
    self.output = LinkOutput()

  def process(self):
    self.anchor = self.parser.parameters['name']
    Label.names[self.anchor] = self
    self.contents = [Constant(' ')]

class Reference(Link):
  "A reference to a label"

  starts = ['\\begin_inset LatexCommand ref', '\\begin_inset CommandInset ref']
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = LinkOutput()
    self.direction = u'↓'

  def process(self):
    key = self.parser.parameters['reference']
    self.url = '#' + key
    if key in Label.names:
      # already seen
      self.direction = u'↑'
    self.contents = [Constant(self.direction)]

ContainerFactory.types += [
    Label, Reference
    ]

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090415
# eLyXer bibliography

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090324
# eLyXer postprocessor code

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090312
# LyX structure in containers



class LyxHeader(Container):
  "Reads the header, outputs the HTML header"

  start = '\\begin_header'
  ending = '\\end_header'

  def __init__(self):
    self.parser = HeaderParser()
    self.output = HeaderOutput()

  def process(self):
    "Find pdf title"
    if '\\pdf_title' in self.parser.parameters:
      Options.title = self.parser.parameters['\\pdf_title']
      Trace.debug('PDF Title: ' + Options.title)

class LyxFooter(Container):
  "Reads the footer, outputs the HTML footer"

  start = '\\end_body'
  ending = '\\end_document'

  def __init__(self):
    self.parser = BoundedDummy()
    self.output = FooterOutput()

class Float(Container):
  "A floating inset"

  start = '\\begin_inset Float'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('div class="float"', True)

  def process(self):
    "Get the float type"
    self.type = self.header[2]
    tagged = TaggedText().complete(self.contents, 'div class="' + self.type + '"')
    self.contents = [tagged]
    caption = self.searchfor(Caption)
    if caption:
      number = NumberGenerator.instance.generatechaptered(self.type)
      prefix = TranslationConfig.floats[self.type]
      layout = caption.contents[0]
      layout.contents.insert(0, Constant(prefix + number + u' '))

class Wrap(Float):
  "A wrapped (floating) float"

  start = '\\begin_inset Wrap'

  def process(self):
    "Get the wrap type"
    Float.process(self)
    placement = self.parameters['placement']
    self.output.tag = 'div class="wrap-' + placement + '"'

class InsetText(Container):
  "An inset of text in a lyx file"

  start = '\\begin_inset Text'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = BoundedParser()
    self.output = ContentsOutput()

class Caption(Container):
  "A caption for a figure or a table"

  start = '\\begin_inset Caption'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('div class="caption"', True)

class Align(Container):
  "Bit of aligned text"

  start = '\\align'
  ending = '\\end_layout'

  def __init__(self):
    self.parser = ExcludingParser()
    self.output = TaggedOutput().setbreaklines(True)

  def process(self):
    self.output.tag = 'div class="' + self.header[1] + '"'

class Space(Container):
  "A space of several types"

  start = '\\begin_inset space'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = FixedOutput()

  def process(self):
    self.type = self.header[2]
    if self.type not in SpaceConfig.spaces:
      Trace.error('Unknown space type ' + self.type)
      self.html = [' ']
      return
    self.html = [SpaceConfig.spaces[self.type]]

class Inset(Container):
  "A generic inset in a LyX document"

  start = '\\begin_inset'
  ending = '\\end_inset'

  def __init__(self):
    self.contents = list()
    self.parser = InsetParser()
    self.output = TaggedOutput().setbreaklines(True)

  def process(self):
    self.type = self.header[1]
    self.output.tag = 'span class="' + self.type + '"'

  def __str__(self):
    return 'Inset of type ' + self.type

class Newline(Container):
  "A newline"

  start = '\\newline'

  def __init__(self):
    self.parser = LoneCommand()
    self.output = FixedOutput()

  def process(self):
    "Process contents"
    self.html = '<br/>'

class NewlineInset(Newline):
  "A newline or line break in an inset"

  start = '\\begin_inset Newline'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = FixedOutput()

class Branch(Container):
  "A branch within a LyX document"

  start = '\\begin_inset Branch'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('span class="branch"', True)

  def process(self):
    "Disable inactive branches"
    self.branch = self.header[2]
    if not self.isactive():
      self.output = EmptyOutput()

  def isactive(self):
    "Check if the branch is active"
    if not self.branch in Options.branches:
      Trace.error('Invalid branch ' + self.branch)
      return True
    branch = Options.branches[self.branch]
    return branch.selected == 1

class ShortTitle(Container):
  "A short title to display (always hidden)"

  start = '\\begin_inset OptArg'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = EmptyOutput()

class Footnote(Container):
  "A footnote to the main text"

  starts = ['\\begin_inset Foot', '\\begin_inset Marginal']
  ending = '\\end_inset'

  order = 0
  list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

  def __init__(self):
    self.parser = InsetParser()
    self.output = ContentsOutput()

  def process(self):
    "Add a letter for the order, rotating"
    letter = Footnote.list[Footnote.order % len(Footnote.list)]
    span = 'span class="FootMarker"'
    fromfoot = TaggedText().constant(u'[→' + letter + u'] ', span)
    self.contents.insert(0, fromfoot)
    tag = TaggedText().complete(self.contents, 'span class="Foot"', True)
    tofoot = TaggedText().constant(' [' + letter + u'→] ', span)
    self.contents = [tofoot, tag]
    Footnote.order += 1

class Note(Container):
  "A LyX note of several types"

  start = '\\begin_inset Note'
  ending = '\\end_inset'

  typetags = {'Note':'', 'Comment':'', 'Greyedout':'span class="greyedout"'}

  def __init__(self):
    self.parser = InsetParser()
    self.output = EmptyOutput()

  def process(self):
    "Hide note and comment, dim greyed out"
    self.type = self.header[2]
    if Note.typetags[self.type] == '':
      return
    self.output = TaggedOutput().settag(Note.typetags[self.type], True)

class Appendix(Container):
  "An appendix to the main document"

  start = '\\start_of_appendix'

  def __init__(self):
    self.parser = LoneCommand()
    self.output = EmptyOutput()

class ListItem(Container):
  "An element in a list"

  starts = ['\\begin_layout Enumerate', '\\begin_layout Itemize']
  ending = '\\end_layout'

  def __init__(self):
    "Output should be empty until the postprocessor can group items"
    self.contents = list()
    self.parser = BoundedParser()
    self.output = EmptyOutput()

  typetags = {'Enumerate':'ol', 'Itemize':'ul'}

  def process(self):
    "Set the correct type and contents."
    self.type = self.header[1]
    tag = TaggedText().complete(self.contents, 'li', True)
    self.contents = [tag]

  def __str__(self):
    return self.type + ' item @ ' + str(self.begin)

class DeeperList(Container):
  "A nested list"

  start = '\\begin_deeper'
  ending = '\\end_deeper'

  def __init__(self):
    self.parser = BoundedParser()
    self.output = ContentsOutput()

  def process(self):
    "Create the deeper list"
    if len(self.contents) == 0:
      Trace.error('Empty deeper list')
      return

  def __str__(self):
    result = 'deeper list @ ' + str(self.begin) + ': ['
    for element in self.contents:
      result += str(element) + ', '
    return result[:-2] + ']'

class ERT(Container):
  "Evil Red Text"

  start = '\\begin_inset ERT'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = EmptyOutput()


ContainerFactory.types += [
    LyxHeader, LyxFooter, InsetText, Caption, Inset, Align, Float, Wrap,
    Newline, Space, NewlineInset, Branch, ShortTitle, Footnote, Appendix,
    Note, ListItem, DeeperList, ERT
    ]

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090411
# LyX layout and derived classes



class Layout(Container):
  "A layout (block of text) inside a lyx file"

  start = '\\begin_layout'
  ending = '\\end_layout'

  typetags = {
      'Quote':'blockquote', 'Standard':'div', 'Part':'h1',
      'Chapter':'h1', 'Section':'h2', 'Subsection':'h3', 'Subsubsection':'h4',
      'Quotation':'blockquote', 'Center':'div', 'Paragraph':'div',
      'LyX-Code':'pre'
      }

  def __init__(self):
    self.contents = list()
    self.parser = BoundedParser()
    self.output = TaggedOutput().setbreaklines(True)

  def process(self):
    self.type = self.header[1]
    if self.type in Layout.typetags:
      self.output.tag = Layout.typetags[self.type] + ' class="' + self.type + '"'
    elif self.type.replace('*', '') in Layout.typetags:
      self.output.tag = Layout.typetags[self.type.replace('*', '')] + ' class="' +  self.type.replace('*', '-') + '"'
    else:
      self.output.tag = 'div class="' + self.type + '"'

  def __str__(self):
    return 'Layout of type ' + self.type

class Title(Layout):
  "The title of the whole document"

  start = '\\begin_layout Title'
  ending = '\\end_layout'

  def process(self):
    self.type = 'title'
    self.output.tag = 'h1 class="title"'
    string = self.searchfor(StringContainer)
    self.title = string.contents[0]
    Trace.message('Title: ' + self.title)

class Author(Layout):
  "The document author"

  start = '\\begin_layout Author'
  ending = '\\end_layout'

  def process(self):
    self.type = 'author'
    self.output.tag = 'h2 class="author"'
    string = self.searchfor(StringContainer)
    FooterOutput.author = string.contents[0]
    Trace.debug('Author: ' + FooterOutput.author)

class FirstWorder(Layout):
  "A layout where the first word is extracted"

  def extractfirstword(self, contents):
    "Extract the first word as a list"
    first, found = self.extractfirsttuple(contents)
    return first

  def extractfirsttuple(self, contents):
    "Extract the first word as a tuple"
    firstcontents = []
    index = 0
    while index < len(contents):
      first, found = self.extractfirstcontainer(contents[index])
      if first:
        firstcontents += first
      if found:
        return firstcontents, True
      else:
        del contents[index]
    return firstcontents, False

  def extractfirstcontainer(self, container):
    "Extract the first word from a string container"
    if len(container.contents) == 0:
      # empty container
      return [container], False
    if isinstance(container, StringContainer):
      return self.extractfirststring(container)
    elif isinstance(container, ERT):
      return [container], False
    if len(container.contents) == 0:
      # empty container
      return None, False
    first, found = self.extractfirsttuple(container.contents)
    if isinstance(container, TaggedText) and hasattr(container, 'tag'):
      newtag = TaggedText().complete(first, container.tag)
      return [newtag], found
    return first, found

  def extractfirststring(self, container):
    "Extract the first word from a string container"
    string = container.contents[0]
    if not ' ' in string:
      return [container], False
    split = string.split(' ', 1)
    container.contents[0] = split[1]
    return [Constant(split[0])], True

class Description(FirstWorder):
  "A description layout"

  start = '\\begin_layout Description'
  ending = '\\end_layout'

  def process(self):
    "Set the first word to bold"
    self.type = 'Description'
    self.output.tag = 'div class="Description"'
    firstword = self.extractfirstword(self.contents)
    if not firstword:
      return
    firstword.append(Constant(u' '))
    tag = 'span class="Description-entry"'
    self.contents.insert(0, TaggedText().complete(firstword, tag))

class List(FirstWorder):
  "A list layout"

  start = '\\begin_layout List'
  ending = '\\end_layout'

  def process(self):
    "Set the first word to bold"
    self.type = 'List'
    self.output.tag = 'div class="List"'
    firstword = self.extractfirstword(self.contents)
    if not firstword:
      return
    tag = 'span class="List-entry"'
    self.contents.insert(0, TaggedText().complete(firstword, tag))

ContainerFactory.types += [
    Layout, Title, Author, Description, List
    ]



class Group(Container):
  "A silly group of containers"

  def __init__(self):
    self.output = ContentsOutput()

  def contents(self, contents):
    self.contents = contents
    return self

  def __str__(self):
    return 'Group: ' + str(self.contents)

class PostNestedList(object):
  "Postprocess a nested list"

  processedclass = DeeperList

  def postprocess(self, deeper, last):
    "Run the postprocessor on the nested list"
    postproc = Postprocessor()
    for index, part in enumerate(deeper.contents):
      result = postproc.postprocess(part)
      deeper.contents[index] = result
    # one additional item to flush the list
    deeper.contents.append(postproc.postprocess(BlackBox()))
    return deeper

class PendingList(object):
  "A pending list"

  def __init__(self):
    self.contents = []
    self.type = None

  def additem(self, item):
    "Add a list item"
    self.contents += item.contents
    self.type = item.type

  def addnested(self, nested):
    "Add a nested list item"
    if self.empty():
      self.insertfake()
    item = self.contents[-1]
    self.contents[-1].contents.append(nested)

  def generatelist(self):
    "Get the resulting list"
    if not self.type:
      return Group().contents(self.contents)
    tag = ListItem.typetags[self.type]
    return TaggedText().complete(self.contents, tag, True)

  def empty(self):
    return len(self.contents) == 0

  def insertfake(self):
    "Insert a fake item"
    item = TaggedText().constant('', 'li class="nested"', True)
    self.contents = [item]
    self.type = 'Itemize'

  def __str__(self):
    result = 'pending ' + str(self.type) + ': ['
    for element in self.contents:
      result += str(element) + ', '
    if len(self.contents) > 0:
      result = result[:-2]
    return result + ']'

class PostListPending(object):
  "Check if there is a pending list"

  def __init__(self):
    self.pending = PendingList()

  def postprocess(self, element, last):
    "If a list element do not return anything;"
    "otherwise return the whole pending list"
    list = None
    if self.generatepending(element):
      list = self.pending.generatelist()
      self.pending.__init__()
    if isinstance(element, ListItem):
      element = self.processitem(element)
    elif isinstance(element, DeeperList):
      element = self.processnested(element)
    if not list:
      return element
    return Group().contents([list, element])

  def processitem(self, item):
    "Process a list item"
    self.pending.additem(item)
    return BlackBox()

  def processnested(self, nested):
    "Process a nested list"
    self.pending.addnested(nested)
    return BlackBox()

  def generatepending(self, element):
    "Decide whether to generate the pending list"
    if self.pending.empty():
      return False
    if isinstance(element, ListItem):
      if not self.pending.type:
        return False
      if self.pending.type != element.type:
        return True
      return False
    if isinstance(element, DeeperList):
      return False
    return True

class PostLayout(object):
  "Numerate an indexed layout"

  processedclass = Layout

  ordered = ['Chapter', 'Section', 'Subsection', 'Subsubsection', 'Paragraph']
  unique = ['Part', 'Book']

  def __init__(self):
    self.generator = NumberGenerator.instance

  def postprocess(self, layout, last):
    "Generate a number and place it before the text"
    if self.containsappendix(layout):
      self.activateappendix()
    if layout.type in PostLayout.unique:
      number = self.generator.generateunique(layout.type)
    elif layout.type in PostLayout.ordered:
      level = PostLayout.ordered.index(layout.type)
      number = self.generator.generate(level)
    elif layout.type == 'Standard':
      return self.checkforfloat(layout)
    else:
      return layout
    layout.contents.insert(0, Constant(number + u' '))
    return layout

  def containsappendix(self, layout):
    "Find out if there is an appendix somewhere in the layout"
    for element in layout.contents:
      if isinstance(element, Appendix):
        return True
    return False

  def activateappendix(self):
    "Change first number to letter, and chapter to appendix"
    self.generator.number = ['-']

  def checkforfloat(self, standard):
    "Check a standard layout for a float inset"
    float = standard.searchfor(Float)
    if not float:
      return standard
    caption = float.searchfor(Caption)
    for layout in caption.contents:
      for element in layout.contents:
        self.movelabel(float, layout, element)
    return float

  def movelabel(self, float, layout, element):
    "Move any labels to the start of the float"
    if not isinstance(element, Label):
      return
    float.contents.insert(0, element)
    index = layout.contents.index(element)
    layout.contents[index] = BlackBox()

class Postprocessor(object):
  "Postprocess an element keeping some context"

  stages = [PostNestedList, PostLayout]
  unconditional = [PostListPending]

  def __init__(self):
    self.stages = self.instantiate(Postprocessor.stages)
    self.stagedict = dict([(x.processedclass, x) for x in self.stages])
    self.unconditional = self.instantiate(Postprocessor.unconditional)
    self.last = None

  def postprocess(self, original):
    "Postprocess an element taking into account the last one"
    element = original
    if element.__class__ in self.stagedict:
      stage = self.stagedict[element.__class__]
      element = stage.postprocess(element, self.last)
    for stage in self.unconditional:
      element = stage.postprocess(element, self.last)
    self.last = original
    return element

  def instantiate(self, classes):
    "Instantiate an element from each class"
    list = [x.__new__(x) for x in classes]
    for element in list:
      element.__init__()
    return list



class BiblioCite(Container):
  "Cite of a bibliography entry"

  starts = ['\\begin_inset LatexCommand cite', '\\begin_inset CommandInset citation']
  ending = '\\end_inset'

  index = 0
  entries = dict()

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('sup')

  def process(self):
    "Add a cite to every entry"
    self.contents = list()
    keys = self.parser.parameters['key'].split(',')
    for key in keys:
      BiblioCite.index += 1
      number = str(BiblioCite.index)
      link = Link().complete(number, 'cite-' + number, '#' + number)
      self.contents.append(link)
      self.contents.append(Constant(','))
      if not key in BiblioCite.entries:
        BiblioCite.entries[key] = []
      BiblioCite.entries[key].append(number)
    if len(keys) > 0:
      # remove trailing ,
      self.contents.pop()

class Bibliography(Container):
  "A bibliography layout containing an entry"

  start = '\\begin_layout Bibliography'
  ending = '\\end_layout'

  def __init__(self):
    self.parser = BoundedParser()
    self.output = TaggedOutput().settag('p class="biblio"', True)

class BiblioEntry(Container):
  "A bibliography entry"

  starts = ['\\begin_inset LatexCommand bibitem', '\\begin_inset CommandInset bibitem']
  ending = '\\end_inset'

  def __init__(self):
    self.parser = InsetParser()
    self.output = TaggedOutput().settag('span class="entry"')

  def process(self):
    "Get all the cites of the entry"
    cites = list()
    key = self.parser.parameters['key']
    if key in BiblioCite.entries:
      cites = BiblioCite.entries[key]
    self.contents = [Constant('[')]
    for cite in cites:
      link = Link().complete(cite, cite, '#cite-' + cite)
      self.contents.append(link)
      self.contents.append(Constant(','))
    if len(cites) > 0:
      self.contents.pop(-1)
    self.contents.append(Constant('] '))

ContainerFactory.types += [
    BiblioCite, Bibliography, BiblioEntry
    ]

class PostBiblio(object):
  "Insert a Bibliography legend before the first item"

  processedclass = Bibliography

  def postprocess(self, element, last):
    "If we have the first bibliography insert a tag"
    if isinstance(last, Bibliography):
      return element
    tag = TaggedText().constant('Bibliography', 'h1 class="biblio"')
    return Group().contents([tag, element])

Postprocessor.stages.append(PostBiblio)

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090207
# eLyXer formula processing

import sys


class Formula(Container):
  "A LaTeX formula"

  start = '\\begin_inset Formula'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = FormulaParser()
    self.output = TaggedOutput().settag('span class="formula"')

  def process(self):
    "Convert the formula to tags"
    pos = Position(self.contents[0])
    whole = WholeFormula()
    if not whole.detect(pos):
      return
    self.contents = [whole]
    whole.parse(pos)
    whole.process()
    if self.header[0] != 'inline':
      self.output.settag('div class="formula"', True)

class FormulaParser(Parser):
  "Parses a formula"

  def parseheader(self, reader):
    "See if the formula is inlined"
    self.begin = reader.linenumber + 1
    if reader.currentline().find('$') > 0:
      return ['inline']
    else:
      return ['block']
  
  def parse(self, reader):
    "Parse the formula"
    if '$' in reader.currentline():
      rest = reader.currentline().split('$', 1)[1]
      if '$' in rest:
        # formula is $...$
        formula = reader.currentline().split('$')[1]
        reader.nextline()
      else:
        # formula is multiline $...$
        formula = self.parsemultiliner(reader, '$', '$')
    elif '\\[' in reader.currentline():
      # formula of the form \[...\]
      formula = self.parsemultiliner(reader, '\\[', '\\]')
    elif '\\begin{' in reader.currentline() and reader.currentline().endswith('}\n'):
      current = reader.currentline().strip()
      endsplit = current.split('\\begin{')[1].split('}')
      startpiece = '\\begin{' + endsplit[0] + '}'
      endpiece = '\\end{' + endsplit[0] + '}'
      formula = self.parsemultiliner(reader, startpiece, endpiece)
    else:
      Trace.error('Formula beginning ' + reader.currentline().strip +
          ' is unknown')
    while not reader.currentline().startswith(self.ending):
      stripped = reader.currentline().strip()
      if len(stripped) > 0:
        Trace.error('Unparsed formula line ' + stripped)
      reader.nextline()
    reader.nextline()
    return [formula]

  def parsemultiliner(self, reader, start, ending):
    "Parse a formula in multiple lines"
    formula = ''
    if not start in reader.currentline():
      Trace.error('Line ' + reader.currentline().strip() +
          ' does not contain formula start ' + start)
      return ''
    index = reader.currentline().index(start)
    formula = reader.currentline()[index + len(start):].strip()
    reader.nextline()
    while not reader.currentline().endswith(ending + '\n'):
      formula += reader.currentline()
      reader.nextline()
    formula += reader.currentline()[:-len(ending) - 1]
    reader.nextline()
    return formula

class Position(object):
  "A position in a formula to parse"

  def __init__(self, text):
    self.text = text
    self.pos = 0

  def skip(self, string):
    "Skip a string"
    self.pos += len(string)

  def remaining(self):
    "Return the text remaining for parsing"
    return self.text[self.pos:]

  def isout(self):
    "Find out if we are out of the formula yet"
    return self.pos >= len(self.text)

  def current(self):
    "Return the current character"
    return self.text[self.pos]

  def checkfor(self, string):
    "Check for a string at the given position"
    if self.pos + len(string) > len(self.text):
      return False
    return self.text[self.pos : self.pos + len(string)] == string

  def clone(self):
    "Return a copy of self"
    clone = Position(self.text)
    clone.pos = self.pos
    return clone

class FormulaBit(Container):
  "A bit of a formula"

  def __init__(self):
    # type can be 'alpha', 'number', 'font'
    self.type = None
    self.original = ''
    self.contents = []
    self.output = ContentsOutput()

  def glob(self, oldpos, check):
    "Glob a bit of text that satisfies a check"
    glob = ''
    pos = oldpos.clone()
    while not pos.isout() and check(pos):
      glob += pos.current()
      pos.skip(pos.current())
    return glob

  def clone(self):
    "Return an exact copy of self"
    type = self.__class__
    clone = type.__new__(type)
    clone.__init__()
    return clone

  def addconstant(self, string, pos):
    "add a constant string"
    self.contents.append(FormulaConstant(string))
    self.addoriginal(string, pos)

  def add(self, bit):
    "Add any kind of formula bit already processed"
    self.contents.append(bit)
    self.original += bit.original

  def addoriginal(self, string, pos):
    "Add a constant to the original formula only"
    self.original += string
    pos.skip(string)

  def __str__(self):
    "Get a string representation"
    return self.__class__.__name__ + ' read in ' + self.original

class TaggedBit(FormulaBit):
  "A tagged string in a formula"

  def constant(self, constant, tag):
    "Set the constant and the tag"
    self.output = TaggedOutput().settag(tag)
    self.add(FormulaConstant(constant))
    return self

  def complete(self, contents, tag):
    "Set the constant and the tag"
    self.contents = contents
    self.output = TaggedOutput().settag(tag)
    return self

class FormulaConstant(FormulaBit):
  "A constant string in a formula"

  def __init__(self, string):
    "Set the constant string"
    FormulaBit.__init__(self)
    self.original = string
    self.output = FixedOutput()
    self.html = [string]

class RawText(FormulaBit):
  "A bit of text inside a formula"

  def detect(self, pos):
    "Detect a bit of raw text"
    return pos.current().isalpha()

  def parse(self, pos):
    "Parse alphabetic text"
    alpha = self.glob(pos, lambda(p): p.current().isalpha())
    self.addconstant(alpha, pos)
    self.type = 'alpha'

class FormulaSymbol(FormulaBit):
  "A symbol inside a formula"

  def detect(self, pos):
    "Detect a symbol"
    if pos.current() in FormulaConfig.unmodified:
      return True
    if pos.current() in FormulaConfig.modified:
      return True
    return False

  def parse(self, pos):
    "Parse the symbol"
    if pos.current() in FormulaConfig.unmodified:
      self.addconstant(pos.current(), pos)
      return
    if pos.current() in FormulaConfig.modified:
      symbol = FormulaConfig.modified[pos.current()]
      self.addoriginal(pos.current(), pos)
      self.contents.append(FormulaConstant(symbol))
      return
    Trace.error('Symbol ' + pos.current() + ' not found')

class Number(FormulaBit):
  "A string of digits in a formula"

  def detect(self, pos):
    "Detect a digit"
    return pos.current().isdigit()

  def parse(self, pos):
    "Parse a bunch of digits"
    digits = self.glob(pos, lambda(p): p.current().isdigit())
    self.addconstant(digits, pos)
    self.type = 'number'

class WholeFormula(FormulaBit):
  "Parse a whole formula"

  # more bits may be appended later
  bits = [ FormulaSymbol(), RawText(), Number() ]

  def __init__(self):
    FormulaBit.__init__(self)
    self.arraymode = False

  def detect(self, pos):
    "Check if inside bounds"
    return not pos.isout()

  def parse(self, pos):
    "Parse with any formula bit"
    while not pos.isout() and not pos.checkfor('}'):
      if self.parsearrayend(pos):
        return
      bit = self.parsebit(pos)
      #Trace.debug(bit.original + ' -> ' + str(bit.gethtml()))
      self.add(bit)

  def parsebit(self, pos):
    "Parse a formula bit"
    for bit in WholeFormula.bits:
      if bit.detect(pos):
        # get a fresh bit and parse it
        newbit = bit.clone()
        newbit.parse(pos)
        return newbit
    Trace.error('Unrecognized formula at ' + pos.remaining())
    constant = FormulaConstant(pos.current())
    pos.skip(pos.current())
    return constant

  def process(self):
    "Process the whole formula"
    for index, bit in enumerate(self.contents):
      bit.process()
      if bit.type == 'alpha':
        # make variable
        self.contents[index] = TaggedBit().complete([bit], 'i')
      elif bit.type == 'font' and index > 0:
        last = self.contents[index - 1]
        if last.type == 'number':
          #separate
          last.contents.append(FormulaConstant(u' '))

  def setarraymode(self):
    "Set array mode for parsing"
    self.arraymode = True
    return self

  def parsearrayend(self, pos):
    "Parse the end of a formula in array mode"
    if not self.arraymode:
      return False
    if pos.checkfor('&'):
      return True
    if pos.checkfor('\\\\'):
      return True
    if pos.checkfor('\\end'):
      return True
    return False

ContainerFactory.types.append(Formula)

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090330
# eLyXer commands in formula processing

import sys


class FormulaCommand(FormulaBit):
  "A LaTeX command inside a formula"
 
  def parsebracket(self, pos):
    "Parse a bracket at the current position"
    bracket = Bracket()
    if not bracket.detect(pos):
      Trace.error('Expected {} at: ' + pos.remaining())
      return
    bracket.parse(pos)
    self.add(bracket)
    return bracket

  def findcommand(self, pos, map):
    "Find any type of command in a map"
    command = self.findalphacommand(pos)
    if command and command in map:
      self.command = command
      return command
    command = self.findsymbolcommand(pos)
    if command and command in map:
      self.command = command
      return command
    return None

  def findalphacommand(self, oldpos):
    "Find a command with \\alpha"
    pos = oldpos.clone()
    if pos.current() != '\\':
      return None
    pos.skip('\\')
    if pos.isout():
      return None
    if not pos.current().isalpha():
      return None
    command = self.glob(pos, lambda(p): p.current().isalpha())
    return '\\' + command

  def findsymbolcommand(self, oldpos):
    "Find a command made with optional \\alpha and one symbol"
    pos = oldpos.clone()
    backslash = ''
    if pos.current() == '\\':
      backslash = '\\'
      pos.skip('\\')
    alpha = self.glob(pos, lambda(p): p.current().isalpha())
    pos.skip(alpha)
    if pos.isout():
      return None
    return backslash + alpha + pos.current()

  def process(self):
    "Process the internals"
    for bit in self.contents:
      bit.process()

class EmptyCommand(FormulaCommand):
  "An empty command (without parameters)"

  def detect(self, pos):
    "Detect the start of an empty command"
    if self.findcommand(pos, FormulaConfig.commands):
      return True
    if self.findcommand(pos, FormulaConfig.alphacommands):
      return True
    return False

  def parse(self, pos):
    "Parse a command without parameters"
    command = self.findcommand(pos, FormulaConfig.commands)
    if command:
      self.addtranslated(command, FormulaConfig.commands, pos)
      return
    command = self.findcommand(pos, FormulaConfig.alphacommands)
    if command:
      self.addtranslated(command, FormulaConfig.alphacommands, pos)
      self.type = 'alpha'
      return
    Trace.error('No command found in ' + pos.remaining())
    return

  def addtranslated(self, command, map, pos):
    "Add a command and find its translation"
    translated = map[command]
    self.addoriginal(command, pos)
    self.contents = [FormulaConstant(translated)]

class OneParamFunction(FormulaCommand):
  "A function of one parameter"

  functions = FormulaConfig.onefunctions

  def detect(self, pos):
    "Detect the start of the function"
    if self.findcommand(pos, self.functions):
      return True
    return False

  def parse(self, pos):
    "Parse a function with one parameter"
    command = self.findcommand(pos, self.functions)
    self.addoriginal(command, pos)
    self.output = TaggedOutput().settag(self.functions[command])
    self.parsebracket(pos)

class FontFunction(OneParamFunction):
  "A function of one parameter that changes the font"

  functions = FormulaConfig.fontfunctions

  def process(self):
    "Do not process the inside"
    self.type = 'font'

class DecoratingFunction(OneParamFunction):
  "A function that decorates some bit of text"

  functions = FormulaConfig.decoratingfunctions

  def parse(self, pos):
    "Parse a decorating function"
    command = self.findcommand(pos, FormulaConfig.decoratingfunctions)
    self.addoriginal(command, pos)
    self.output = TaggedOutput().settag('span class="withsymbol"')
    self.type = 'alpha'
    symbol = FormulaConfig.decoratingfunctions[command]
    tagged = TaggedBit().constant(symbol, 'span class="symbolover"')
    self.contents.append(tagged)
    bracket = self.parsebracket(pos)
    bracket.output = TaggedOutput().settag('span class="undersymbol"')
    # simplify if possible
    if self.original in FormulaConfig.alphacommands:
      self.output = FixedOutput()
      self.html = FormulaConfig.alphacommands[self.original]

class TwoParamFunction(FormulaCommand):
  "A function of two parameters"

  def detect(self, pos):
    "Detect the start of the function"
    if self.findcommand(pos, FormulaConfig.twofunctions):
      return True
    return False

  def parse(self, pos):
    "Parse a function of two parameters"
    command = self.findcommand(pos, FormulaConfig.twofunctions)
    self.addoriginal(command, pos)
    tags = FormulaConfig.twofunctions[command]
    self.output = TaggedOutput().settag(tags[0])
    bracket1 = self.parsebracket(pos)
    if not bracket1:
      return
    bracket1.output = TaggedOutput().settag(tags[1])
    bracket2 = self.parsebracket(pos)
    if not bracket2:
      return
    bracket2.output = TaggedOutput().settag(tags[2])

class Bracket(FormulaBit):
  "A {} bracket inside a formula"

  def detect(self, pos):
    "Detect the start of a bracket"
    return pos.checkfor('{')

  def parse(self, pos):
    "Parse the bracket"
    self.addoriginal('{', pos)
    self.inside = WholeFormula()
    if not self.inside.detect(pos):
      Trace.error('Dangling {')
      return
    self.inside.parse(pos)
    self.add(self.inside)
    if pos.isout() or pos.current() != '}':
      Trace.error('Missing } in ' + pos.remaining())
      return
    self.addoriginal('}', pos)

  def process(self):
    "Process the bracket"
    self.inside.process()

WholeFormula.bits += [
    EmptyCommand(), OneParamFunction(), DecoratingFunction(),
    TwoParamFunction(), FontFunction(), 
    ]

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090207
# eLyXer tables



class Table(Container):
  "A lyx table"

  start = '\\begin_inset Tabular'
  ending = '\\end_inset'

  def __init__(self):
    self.parser = TableParser()
    self.output = TaggedOutput().settag('table', True)

class Row(Container):
  "A row in a table"

  start = '<row'
  ending = '</row'

  def __init__(self):
    self.parser = TablePartParser()
    self.output = TaggedOutput().settag('tr', True)
    self.columns = list()

  def setcolumns(self, columns):
    "Process alignments for every column"
    if len(columns) != len(self.contents):
      Trace.error('Columns: ' + str(len(columns)) + ', cells: ' + str(len(self.contents)))
      return
    for index, column in enumerate(columns):
      alignment = column['alignment']
      if alignment == 'block':
        alignment = 'justify'
      self.contents[index].setalignment(alignment)
      valignment = column['valignment']
      self.contents[index].setvalignment(valignment)

class Cell(Container):
  "A cell in a table"

  start = '<cell'
  ending = '</cell'

  def __init__(self):
    self.parser = TablePartParser()
    self.output = TaggedOutput().settag('td', True)

  def setmulticolumn(self, span):
    "Set the cell as multicolumn"
    self.setattribute('colspan', span)

  def setalignment(self, alignment):
    "Set the alignment for the cell"
    self.setattribute('align', alignment)

  def setvalignment(self, valignment):
    "Set the vertical alignment"
    self.setattribute('valign', valignment)

  def setattribute(self, attribute, value):
    "Set a cell attribute in the tag"
    self.output.tag += ' ' + attribute + '="' + unicode(value) + '"'

class TableParser(BoundedDummy):
  "Parse the whole table"

  ending = '</lyxtabular'
  column = '<column'

  def __init__(self):
    BoundedDummy.__init__(self)
    self.columns = list()

  def parse(self, reader):
    "Parse table header as parameters, rows and end of table"
    contents = []
    while not self.checkcurrent(reader, TableParser.ending):
      if self.checkcurrent(reader, Row.start):
        row = self.factory.create(reader)
        row.setcolumns(self.columns)
        contents.append(row)
      elif self.checkcurrent(reader, TableParser.column):
        self.parsecolumn(reader)
      else:
        self.parseparameter(reader)
    BoundedDummy.parse(self, reader)
    return contents

  def checkcurrent(self, reader, start):
    "Check if the current line starts with the given string"
    return reader.currentline().strip().startswith(start)

  def parsecolumn(self, reader):
    "Parse a column definition"
    self.parseparameter(reader)
    self.columns.append(self.parameters['column'])

class TablePartParser(BoundedParser):
  "Parse a table part (row or cell)"

  def parseheader(self, reader):
    "Parse the header"
    self.parsexml(reader)
    parameters = dict()
    if len(self.parameters) > 1:
      Trace.error('Too many parameters in table part')
    for key in self.parameters:
      parameters = self.parameters[key]
    self.parameters = parameters
    return list()

ContainerFactory.types += [Table, Row, Cell]

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090308
# eLyXer image treatment

import os
import os.path
import struct


class Image(Container):
  "An embedded image"

  start = '\\begin_inset Graphics'
  ending = '\\end_inset'

  converter = True

  def __init__(self):
    self.parser = InsetParser()
    self.output = ImageOutput()

  def process(self):
    self.url = self.parser.parameters['filename']
    origin = self.getpath(self.url)
    if not os.path.exists(origin):
      Trace.error('Image ' + origin + ' not found')
      return
    self.destination = self.getdestination(self.url)
    destination = self.getpath(self.destination)
    density = 100
    if 'scale' in self.parser.parameters:
      density = int(self.parser.parameters['scale'])
    self.convert(origin, destination, density)
    self.width, self.height = self.getdimensions(destination)

  def getpath(self, path):
    "Get the correct path for the image"
    if os.path.isabs(path):
      return path
    return Options.directory + os.path.sep + path

  def getdestination(self, origin):
    "Get the destination URL for an image URL"
    if os.path.isabs(origin):
      dest = os.path.basename(origin)
    else:
      dest = origin
    return os.path.splitext(dest)[0] + '.png'

  def convert(self, origin, destination, density):
    "Convert an image to PNG"
    if not Image.converter:
      return
    if origin == destination:
      return
    if os.path.exists(destination):
      if os.path.getmtime(origin) <= os.path.getmtime(destination):
        # file has not changed; do not convert
        return
    dir = os.path.dirname(destination)
    if len(dir) > 0 and not os.path.exists(dir):
      os.makedirs(dir)
    try:
      result = os.system('convert -density ' + str(density) + ' "' + origin +
          '" "' + destination + '"')
      if result != 0:
        Trace.error('ImageMagick not installed; images will not be processed')
        Image.converter = False
        return
      Trace.message('Converted ' + origin + ' to ' + destination + ' at ' +
          str(density) + '%')
    except OSError:
      Trace.error('Error while converting image ' + origin)

  dimensions = dict()

  def getdimensions(self, filename):
    "Get the dimensions of a PNG image"
    if not os.path.exists(filename):
      return None, None
    if filename in Image.dimensions:
      return Image.dimensions[filename]
    pngfile = codecs.open(filename, 'rb')
    pngfile.seek(16)
    width = self.readlong(pngfile)
    height = self.readlong(pngfile)
    dimensions = (width, height)
    pngfile.close()
    Image.dimensions[filename] = dimensions
    return dimensions

  def readlong(self, file):
    "Read a long value"
    tuple = struct.unpack('>L', file.read(4))
    return tuple[0]

class ImageOutput(object):
  "Returns an image in the output"

  def gethtml(self, container):
    "Get the HTML output of the image as a list"
    cssclass = 'embedded'
    html = ['<img class="' + cssclass + '"']
    if hasattr(container, 'destination'):
      html.append(' src="' + container.destination +
          '" alt="figure ' + container.destination + '" width="' +
          str(container.width) + '" height="' + str(container.height) + '"')
    else:
      html.append(' src="' + container.url + '"')
    html.append('/>\n')
    return html

ContainerFactory.types.append(Image)

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090420
# eLyXer postprocessor for tables



class PostTable(object):
  "Postprocess a table"

  def postprocess(self, current, last):
    "Look for a table and postprocess it"
    tables = current.searchall(Table)
    if len(tables) == 0:
      return current
    for table in current.searchall(Table):
      self.posttable(table)
    return current

  def posttable(self, table):
    "Postprocess the table"
    self.longtable(table)
    for row in table.contents:
      index = 0
      while index < len(row.contents):
        self.checkmulticolumn(row, index)
        index += 1

  def longtable(self, table):
    "Postprocess a long table, removing unwanted rows"
    if not 'features' in table.parameters:
      return
    features = table.parameters['features']
    if not 'islongtable' in features:
      return
    if features['islongtable'] != 'true':
      return
    if self.hasrow(table, 'endfirsthead'):
      self.removerows(table, 'endhead')
    if self.hasrow(table, 'endlastfoot'):
      self.removerows(table, 'endfoot')

  def hasrow(self, table, attrname):
    "Find out if the table has a row of first heads"
    for row in table.contents:
      if attrname in row.parameters:
        return True
    return False

  def removerows(self, table, attrname):
    "Remove the head rows, since the table has first head rows."
    for row in table.contents:
      if attrname in row.parameters:
        row.output = EmptyOutput()

  def checkmulticolumn(self, row, index):
    "Process a multicolumn attribute"
    cell = row.contents[index]
    if not 'multicolumn' in cell.parameters:
      return
    mc = cell.parameters['multicolumn']
    if mc != '1':
      Trace.error('Unprocessed multicolumn=' + str(multicolumn) + ' cell ' + str(cell))
      return
    total = 1
    index += 1
    while self.checkbounds(row, index):
      del row.contents[index]
      total += 1
    cell.setmulticolumn(total)

  def checkbounds(self, row, index):
    "Check if the index is within bounds for the row"
    if index >= len(row.contents):
      return False
    if not 'multicolumn' in row.contents[index].parameters:
      return False
    if row.contents[index].parameters['multicolumn'] != '2':
      return False
    return True

Postprocessor.unconditional.append(PostTable)

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090422
# eLyXer postprocessor for formulae



class PostFormula(object):
  "Postprocess a formula"

  limited = ['\\sum', '\\int', '\\intop']
  limits = ['^', '_']

  def postprocess(self, current, last):
    "Postprocess any formulae"
    for formula in current.searchall(Formula):
      self.postcontents(formula.contents)
    return current

  def postcontents(self, contents):
    "Search for sum or integral"
    for index, bit in enumerate(contents):
      self.checklimited(contents, index)
      self.checkroot(contents, index)
      if isinstance(bit, FormulaBit):
        self.postcontents(bit.contents)

  def checklimited(self, contents, index):
    "Check for a command with limits"
    bit = contents[index]
    if not hasattr(bit, 'command'):
      return
    if not bit.command in PostFormula.limited:
      return
    limits = self.findlimits(contents, index + 1)
    limits.reverse()
    if len(limits) == 0:
      return
    tagged = TaggedText().complete(limits, 'span class="limits"')
    contents.insert(index + 1, tagged)

  def findlimits(self, contents, index):
    "Find the limits for the command"
    limits = []
    while index < len(contents):
      if not self.checklimits(contents, index):
        return limits
      limits.append(contents[index])
      del contents[index]
    return limits

  def checklimits(self, contents, index):
    "Check for a command making the limits"
    bit = contents[index]
    if not hasattr(bit, 'command'):
      return False
    if not bit.command in PostFormula.limits:
      return False
    bit.output.tag += ' class="bigsymbol"'
    return True

  def checkroot(self, contents, index):
    "Check for a root, insert the radical in front"
    bit = contents[index]
    if not hasattr(bit, 'original'):
      return
    if not bit.original.startswith('\\sqrt'):
      return
    radical = TaggedText().constant(u'√', 'span class="radical"')
    root = TaggedText().complete(bit.contents, 'span class="root"')
    bit.contents = [radical, root]

Postprocessor.unconditional.append(PostFormula)

#! /usr/bin/env python
# -*- coding: utf-8 -*-

#   eLyXer -- convert LyX source files to HTML output.
#
#   Copyright (C) 2009 Alex Fernández
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# --end--
# Alex 20090427
# eLyXer arrays in formulae

import sys


class FormulaCell(FormulaCommand):
  "An array cell inside a row"

  def __init__(self, alignment):
    FormulaCommand.__init__(self)
    self.alignment = alignment
    self.output = TaggedOutput().settag('td class="formula-' + alignment +'"', True)

  def parse(self, pos):
    formula = WholeFormula().setarraymode()
    if not formula.detect(pos):
      Trace.error('Unexpected end of array cell at ' + pos.remaining())
      return
    formula.parse(pos)
    self.add(formula)

class FormulaRow(FormulaCommand):
  "An array row inside an array"

  def __init__(self, alignments):
    FormulaCommand.__init__(self)
    self.alignments = alignments
    self.output = TaggedOutput().settag('tr', True)

  def parse(self, pos):
    for i in self.alignments:
      cell = FormulaCell(i)
      cell.parse(pos)
      self.add(cell)
      if pos.checkfor('&'):
        self.addoriginal('&', pos)

class FormulaArray(FormulaCommand):
  "An array within a formula"

  start = '\\begin{array}'
  ending = '\\end{array}'

  def __init__(self):
    FormulaCommand.__init__(self)
    self.output = TaggedOutput().settag('table class="formula"', True)
    self.valign = 'c'

  def detect(self, pos):
    "Detect an array"
    if not pos.checkfor(FormulaArray.start):
      return False
    return True

  def parse(self, pos):
    "Parse the array"
    self.addoriginal(FormulaArray.start, pos)
    self.parsealignments(pos)
    self.contents.pop()
    while not pos.isout():
      row = FormulaRow(self.alignments)
      row.parse(pos)
      self.add(row)
      if pos.checkfor(FormulaArray.ending):
        self.addoriginal(FormulaArray.ending, pos)
        return
      self.parserowend(pos)

  def parsealignments(self, pos):
    "Parse the different alignments"
    # vertical
    if pos.checkfor('['):
      self.addoriginal('[', pos)
      self.valign = pos.current()
      self.addoriginal(self.valign, pos)
      if not pos.checkfor(']'):
        Trace.error('Vertical alignment ' + self.valign + ' not closed')
      self.addoriginal(']', pos)
    # horizontal
    bracket = self.parsebracket(pos)
    if not bracket:
      Trace.error('No alignments for array in ' + pos.remaining())
      return
    self.alignments = []
    for l in bracket.original[1:-1]:
      self.alignments.append(l)

  def parserowend(self, pos):
    "Parse the end of a row"
    if not pos.checkfor('\\\\'):
      Trace.error('No row end at ' + pos.remaining())
      self.addoriginal(pos.current(), pos)
      return
    self.addoriginal('\\\\', pos)

class FormulaCases(FormulaArray):
  "A cases statement"

  start = '\\begin{cases}'
  ending = '\\end{cases}'

  def __init__(self):
    FormulaCommand.__init__(self)
    self.output = TaggedOutput().settag('table class="cases"', True)
    self.alignments = ['l', 'l']

  def detect(self, pos):
    "Detect a cases statement"
    if not pos.checkfor(FormulaCases.start):
      return False
    return True

  def parse(self, pos):
    "Parse the cases"
    self.addoriginal(FormulaCases.start, pos)
    while not pos.isout():
      row = FormulaRow(self.alignments)
      row.parse(pos)
      self.add(row)
      if pos.checkfor(FormulaCases.ending):
        self.addoriginal(FormulaCases.ending, pos)
        return
      self.parserowend(pos)

WholeFormula.bits += [FormulaArray(), FormulaCases()]



class Book(object):
  "book in a lyx file"

  def parsecontents(self, reader, writer):
    "Parse the contents of the reader and write them"
    factory = ContainerFactory()
    postproc = Postprocessor()
    while not reader.finished():
      container = factory.create(reader)
      container = postproc.postprocess(container)
      writer.write(container.gethtml())

def createbook(args):
  "Read a whole book, write it"
  filein = sys.stdin
  fileout = sys.stdout
  if len(args) < 2:
    Options.quiet = True
  if len(args) > 0:
    Options.directory = os.path.dirname(args[0])
    if Options.directory == '':
      Options.directory = '.'
    filein = codecs.open(args[0], 'r', "utf-8")
    del args[0]
  if len(args) > 0:
    fileout = codecs.open(args[0], 'w', "utf-8")
    del args[0]
  if len(args) > 0:
    usage('Too many arguments')
    return
  reader = LineReader(filein)
  writer = HtmlWriter(fileout)
  book = Book()
  book.parsecontents(reader, writer)

def usage(error):
  "Show an error message and correct usage"
  if not error:
    return
  Trace.error(error)
  Trace.error('Usage: eLyXer [filein] [fileout].')
  Trace.error('  Options:')
  Trace.error('    --nocopy: disables the copyright notice at the bottom')
  Trace.error('    --quiet: disables all runtime messages')
  Trace.error('    --debug: enable debugging messages (for developers)')
  Trace.error('    --title <title>: set the generated page title')
  Trace.error('    --css <file.css>: use a custom CSS file')
  exit()

biblio = dict()
args = sys.argv
del args[0]
error = Options().parseoptions(args)
if error:
  usage(error)
  exit()
createbook(args)

