"""$URL: svn://svn/repos/trunk/sitereaper/lib/validate.py $
$Id: validate.py 20952 2003-03-05 16:51:11Z dbinger $

Provides code to wrap nsgmls, for validation of HTML and XHTML
documents.
"""
import os, re
from sitereaper.utils import warn, _exec

# Need to recognize an XML declaration line in order to ignore it.
# W3C strongly encourages use of XML declaration lines in XHTML
# documents (see XHTML1 spec, 3.1.1).
xml_decl_re = re.compile(r'<\?xml[^>]*\?>', re.IGNORECASE)

# The document type declaration line is essential -- it's how
# we distinguish HTML 4 from XHTML.  A document without this
# line cannot be validated.
doctype_decl_re = re.compile(
    r'\<\!DOCTYPE \s+ HTML \s+ PUBLIC \s+'  # fixed stuff
    r'\"([^\"]+)\"'                         # document type
    r'(?: \s+ \"([^\"]+)\")? \s*>',         # DTD URI (I think)
    re.VERBOSE|re.IGNORECASE)

# Doctypes for HTML 4.0, 4.01, and XHTML 1.0:
#  -//W3C//DTD HTML 4.0//EN
#  -//W3C//DTD HTML 4.0 Transitional//EN
#  -//W3C//DTD HTML 4.0 Frameset//EN
#  -//W3C//DTD HTML 4.01//EN
#  -//W3C//DTD HTML 4.01 Transitional//EN
#  -//W3C//DTD HTML 4.01 Frameset//EN
#  -//W3C//DTD XHTML 1.0 Strict//EN
#  -//W3C//DTD XHTML 1.0 Transitional//EN
#  -//W3C//DTD XHTML 1.0 Frameset//EN
doctype_re = re.compile(r'-//W3C//DTD (X?HTML) ([^/]+)//EN')

nsgmls_err_re = re.compile(r'\w+:([^:]+):(\d+):(\d+):(?:([EW]):)? (.*)')

# Every Validator needs to dump stdout to /dev/null, so just open it once.
DEVNULL = os.open("/dev/null", os.O_WRONLY)

EXPAND_ETYPE = {"E": "error",
                "W": "warning"}


def spawn_pipe_nsgmls (argv):
    """
    Spawn a child process writing stdout to /dev/null.  Return a tuple
      (pid, child_stderr)
    where pid is the PID of the child and child_stderr is a
    readable file object connected to the child's stderr.
    """
    (err_r, err_w) = os.pipe()
    pid = os.fork()
    if pid == 0:                        # in the child
        os.dup2(DEVNULL, 1)             # stdout to /dev/null
        os.dup2(err_w, 2)               # connect stderr to parent
        os.close(err_r)
        _exec(argv)
    else:                               # in the parent
        os.close(err_w)
        child_stderr = os.fdopen(err_r, "r", -1)
        return (pid, child_stderr)



class Validator:

    def __init__ (self, filename):
        self.filename = filename
        self.doctype_error = None
        self.lang = None                # eg. "HTML" or "XHTML"
        self.version = None             # eg. "4.0 Transitional"
        self.results = []
        self._guess_doctype()

    def _guess_doctype (self):
        decl = ''
        num_lines = 0
        file = open(self.filename)
        while 1:
            line = file.readline()
            if xml_decl_re.search(line):
                continue

            decl += line
            num_lines += 1
            decl_match = doctype_decl_re.search(decl)
            if decl_match or num_lines > 5:
                break

        if decl_match:
            doc_type = decl_match.group(1)
            match = doctype_re.match(doc_type)
            if match:
                (self.lang, self.version) = match.group(1, 2)
            else:
                self.doctype_error = "invalid document type: %r" % doc_type
        else:
            self.doctype_error = "couldn't determine document type"

        file.close()

    def validate (self):
        if self.doctype_error:
            return

        # Validation command for HTML:
        #   nsgmls -E0 -wnon-sgml-char-ref foo.html > /dev/null
        # for XHTML:
        #   nsgmls -E0 -wxml -c /www/misc/sgml/xhtml.soc foo.html > /dev/null

        cmd = ["nsgmls", "-E0"]
        if self.lang == "HTML":
            cmd += ["-wnon-sgml-char-ref"]
        elif self.lang == "XHTML":
            cmd += ["-wxml", "-c", "/www/misc/sgml/xhtml.soc"]
        else:
            # doctype_re guards against this
            raise RuntimeError(
                "this can't happen! (self.lang == %r)" % self.lang)
        cmd.append(self.filename)

        (pid, errors) = spawn_pipe_nsgmls(cmd)
        while 1:
            line = errors.readline()
            if not line:
                break
            match = nsgmls_err_re.match(line)
            if not match:
                warn("couldn't parse nsgmls error message: %r" % line)
                continue
            (filename, line, col, etype, msg) = match.groups()
            # nsgmls has been known to issue warnings about the
            # various SGML bureaucracy files (catalogs, DTDs, etc.)
            if filename == self.filename:
                self.results.append((line, col, etype, msg))

        errors.close()
        (pid, status) = os.waitpid(pid, 0)

        # nsgmls exits with status 1 (256 in 16-bit status word)
        # when it finds errors, so ignore that status code.
        if status not in (0, 256):
            warn("nsgmls exited with status %d" % status)

    def format_results (self):
        if self.doctype_error:
            return ["error: " + self.doctype_error]
        else:
            results = []
            for (line, col, etype, msg) in self.results:
                if etype:
                    etype = EXPAND_ETYPE.get(etype) + ": "
                else:
                    etype = ""
                results.append("line %s, col %s: %s%s"
                               % (line, col, etype, msg))
        return results

if __name__ == "__main__":
    import sys
    from urllib import urlretrieve

    for name in sys.argv[1:]:
        (filename, headers) = urlretrieve(name)
        val = Validator(filename)
        val.validate()
        errors = val.format_results()
        if errors:
            print "%s:" % name
            for r in errors:
                print "  " + r
