This nifty little piece of code took about half an hour to write (which was mostly spent grovelling through vim docs) and about a week to debug. Eventually I discovered that the XML parser (overkill, I know, but see Rule 37) was converting & to &, along with all the other HTML special chars.

Update: I've finally added an optional parameter to specify the type of code. This was necessitated by the update to my squid proxy page which required more than one squid.conf. Also, it fixes a possible security issue which would have made any php snippets I put up here interpretable by the webserver. Now I can call them something else and still syntax highlight them as php. And it was only three additional lines of code and two other modifications. Shiny!

<?php
    function vim_shl($filename, $syntype=NULL) {
        // This function uses vim to syntax highlight a the contents of
        // $filename.  The HTML generated by vim is returned.
        //
        // Note:  Since vim detects filetype mostly by file extension, the
        // extension of $filename should match the type of highlighting.  A
        // special case is the handling of various configuration files where
        // $filename *must* be the actual name of the config file.
        //
        // TODO:
        // * some method of passing a small chunk of text rather than a whole
        //   file.  This probably requires refactoring somewhat.

        $tfHighlight = tempnam("/tmp", "shl");
        $tfVimCommands = tempnam("/tmp", "shl");
        $fpVimCommands = fopen($tfVimCommands,'wb');
        fwrite($fpVimCommands, ":syn on\n"); // turn on syntax highlighting
        if ($syntype != NULL) {
            fwrite($fpVimCommands, ":set syn=$syntype\n"); // set syntax type
        }
        fwrite($fpVimCommands, ":let use_xhtml = 1\n"); // output XHTML
        fwrite($fpVimCommands, ":let html_use_css = 1\n"); // output CSS
        fwrite($fpVimCommands, ":set ts=4\n"); // set tabstops to 4 (ymmv)
        fwrite($fpVimCommands, ":let @s = '<syntaxhl codetype=\"' . b:current_syntax . '\"></syntaxhl>'\n");
        fwrite($fpVimCommands, ":run! syntax/2html.vim\n"); // convert to HTML
        fwrite($fpVimCommands, ":1d\n"); // Delete the first line (CSS HTML 4
                                         // header, breaks XML parsing)
        fwrite($fpVimCommands, ":3put S\n");
        fwrite($fpVimCommands, "%s/&/\&amp;/g\n"); // Double escape &entity;
                                                   // for XML parser 
        fwrite($fpVimCommands, ":wq! $tfHighlight\n"); // save and quit
                                                       // (highlighted stuff)
        fwrite($fpVimCommands, ":q\n"); // quit without saving (original)
        fclose($fpVimCommands);
        system("vim -efns < $tfVimCommands $filename");
        $hlText = file_get_contents($tfHighlight);
        unlink($tfVimCommands);
        unlink($tfHighlight);
        return $hlText;
    }

    $xhtml_print = false;
    $code_type = "";

    function handle_start_elem($parser, $name, $attribs) {
        global $xhtml_print;
        global $code_type;
        $name = strtolower($name);

        // Get code type (to allow different displays for different languages)
        if ($name == "syntaxhl") {
            $code_type = $attribs["CODETYPE"];
        }

        // Cut everything before the <pre> tag
        if ($name == "pre") {
            $code_style = "hlcode";
            if ($code_type) {
                $code_style .= " hl$code_type";
            }
            $attribs = array("class" => $code_style);
            $xhtml_print = true;
        }

        // Put HTML tags back
        if ($xhtml_print) {
            echo "<$name";
            if (sizeof($attribs)) {
                while (list($k, $v) = each($attribs)) {
                    $k = strtolower($k);
                    echo " $k=\"$v\"";
                }
            }
            echo ">";
        }
    }

    function handle_character_data($parser, $data) {
        // Anything not XML structure stays
        global $xhtml_print;
        if ($xhtml_print) {
            echo($data);
        }
    }

    function handle_end_elem($parser, $name) {
        global $xhtml_print;
        $name = strtolower($name);

        // Put closing tags back
        if ($xhtml_print) {
            echo "</$name>";
        }

        // Stop after the </pre> -- we don't want any of the closing stuff
        if ($name == "pre") {
            $xhtml_print = false;
        }
    }

    function parse_xhtml($xhtmlstream) {
        // HTML generated by vim (and returned by vim_shl()) is not really
        // suitable for our needs.  Thus, since it's XML and PHP kindly
        // provides a parser, we can play with the HTML somewhat before
        // displaying it.

        $xml_parser = xml_parser_create();
        xml_set_element_handler($xml_parser, "handle_start_elem", "handle_end_elem");
        xml_set_character_data_handler($xml_parser, "handle_character_data");
        if (!xml_parse($xml_parser, $xhtmlstream)) {
            die(sprintf("XML error: %s at line %d\n",
                    xml_error_string(xml_get_error_code($xml_parser)),
                    xml_get_current_line_number($xml_parser)));
            xml_parser_free($xml_parser);
        }
    }

    function syntaxhl($filename, $syntype=NULL) {
        parse_xhtml(vim_shl($filename, $syntype));
    }
?>