summaryrefslogtreecommitdiffstatshomepage
path: root/src/tools/src2html.cpp
blob: b6db0879057d1ea859793f6389c8b8b849b70da1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
Using MAME
----------

If you want to dive right in and skip the command line, there's a nice graphical way to use MAME without the need to download and set up a front end. Simply start MAME with no parameters, by doubleclicking the **mame.exe** file or running it directly from the command line. If you're looking to harness the full power of MAME, keep reading further.

On Macintosh OS X and \*nix-based platforms, please be sure to set your font up to match your locale before starting, otherwise you may not be able to read the text due to missing glyphs.

If you are a new MAME user, you could find this emulator a bit complex at first. Let's take a moment to talk about softlists, as they can simplify matters quite a bit. If the content you are trying to play is a documented entry on one of the MAME softlists, starting the content is as easy as

    **mame.exe** *<system>* *<software>*

For instance:

    **mame.exe nes metroidu**

will load the USA version of Metroid for the Nintendo Entertainment System.


Alternatively, you could start MAME with

	**mame.exe nes**
	
and choose the *software list* from the cartridge slot. From there, you could pick any softlist-compatible software you have in your roms folders. Please note that many older dumps of cartridges and discs may either be bad or require renaming to match up to the softlist in order to work in this way.


If you are loading an arcade board or other non-softlist content, things are only a little more complicated:


The basic usage, from command line, is

	**mame.exe** *<system>* *<media>* *<software>* *<options>*

where

* *<system>* is the shortname of the system you want to emulate (e.g. nes, c64, etc.)
* *<media>* is the switch for the media you want to load (if it's a cartridge, try **-cart** or **-cart1**; if it's a floppy disk, try **-flop** or **-flop1**; if it's a CD-ROM, try **-cdrom**)
* *<software>* is the program / game you want to load (and it can be given either as the fullpath to the file to load, or as the shortname of the file in our software lists)
* *<options>* is any additional command line option for controllers, video, sound, etc.

Remember that if you type a <system> name which does not correspond to any emulated system, MAME will suggest you some possible choices which are close to what you typed; and if you don't know which <media> switch are available, you can always launch

	**mame.exe** *<system>* **-listmedia**

If you don't know what *<options>* are available, there are a few things you can do. First of all, you can check the command line options section of this manual. You can also try one of the many :ref:`frontends` available for MAME.


Alternatively, you should keep in mind the following command line options, which might be very useful on occasion:


	**mame.exe -help**

tells what MAME is the basic structure of MAME launching options, i.e. as explained above.


	**mame.exe -showusage**

gives you the (quite long) list of available command line options for MAME. The main options are described, in the :ref:`universal-command-line` section of this manual.


	**mame.exe -showconfig**

gives you a (quite long) list of available configuration options for MAME. These configuration can always be modified at command line, or by editing them in mame.ini which is the main configuration file for MAME. You can find a description of some configuration options in the :ref:`universal-command-line` section of the manual (in most cases, each configuration option has a corresponding command line option to configure and modify it).


	**mame.exe -createconfig**

creates a brand new **mame.ini** file, with default configuration settings. Notice that mame.ini is basically a plain text file, hence you can open it with any text editor (e.g. Notepad, Emacs or TextEdit) and configure every option you need. However, no particular tweaks are needed to start, so you can basically leave most of the options unaltered.

If you execute **mame64 -createconfig** when you already have an existing mame.ini from a previous MAME version, MAME automatically updates the pre-existing mame.ini by copying changed options into it.


Once you are more confident with MAME options, you may want to configure a bit more your setup. In this case, keep in mind the order in which options are read; see :ref:`advanced-multi-CFG` for details.
11'>411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
// license:BSD-3-Clause
// copyright-holders:Aaron Giles
/***************************************************************************

    MAME source code to HTML converter

****************************************************************************

    Template file format:

    <html header>
    <!--PATH--> = insert path
    <!--CONTENT--> = insert content
    <html footer>

***************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
#include "osdcore.h"
#include "corestr.h"
#include "corefile.h"


/***************************************************************************
    CONSTANTS & DEFINES
***************************************************************************/

#define PREPROC_CLASS       "preproc"
#define KEYWORD_CLASS       "keyword"
#define CUSTOM_CLASS        "custom"



/***************************************************************************
    TYPE DEFINITIONS
***************************************************************************/

enum file_type
{
	FILE_TYPE_INVALID,
	FILE_TYPE_C,
	FILE_TYPE_MAKE,
	FILE_TYPE_XML,
	FILE_TYPE_TEXT
};


struct ext_to_type
{
	const char *    extension;
	file_type       type;
};


struct token_entry
{
	const char *    token;
	const char *    color;
};


struct include_path
{
	include_path *  next;
	std::string     path;
};


struct list_entry
{
	list_entry *    next;
	std::string     name;
};



/***************************************************************************
    GLOBAL VARIABLES
***************************************************************************/

static include_path *incpaths;

static const ext_to_type extension_lookup[] =
{
	{ ".c", FILE_TYPE_C },
	{ ".h", FILE_TYPE_C },
	{ ".lst", FILE_TYPE_C },
	{ ".mak", FILE_TYPE_MAKE },
	{ "makefile", FILE_TYPE_MAKE },
	{ ".lay", FILE_TYPE_XML },
	{ ".xml", FILE_TYPE_XML },
	{ ".txt", FILE_TYPE_TEXT },
};


static const token_entry dummy_token_table[] =
{
	{ nullptr, KEYWORD_CLASS }
};


static const token_entry c_token_table[] =
{
	{ "#define", PREPROC_CLASS },
	{ "#elif", PREPROC_CLASS },
	{ "#else", PREPROC_CLASS },
	{ "#endif", PREPROC_CLASS },
	{ "#error", PREPROC_CLASS },
	{ "#if", PREPROC_CLASS },
	{ "#ifdef", PREPROC_CLASS },
	{ "#ifndef", PREPROC_CLASS },
	{ "#include", PREPROC_CLASS },
	{ "#line", PREPROC_CLASS },
	{ "#pragma", PREPROC_CLASS },
	{ "#undef", PREPROC_CLASS },

	{ "auto", KEYWORD_CLASS },
	{ "bool", KEYWORD_CLASS },
	{ "break", KEYWORD_CLASS },
	{ "case", KEYWORD_CLASS },
	{ "catch", KEYWORD_CLASS },
	{ "char", KEYWORD_CLASS },
	{ "class", KEYWORD_CLASS },
	{ "const", KEYWORD_CLASS },
	{ "const_cast", KEYWORD_CLASS },
	{ "continue", KEYWORD_CLASS },
	{ "default", KEYWORD_CLASS },
	{ "delete", KEYWORD_CLASS },
	{ "do", KEYWORD_CLASS },
	{ "double", KEYWORD_CLASS },
	{ "dynamic_cast", KEYWORD_CLASS },
	{ "else", KEYWORD_CLASS },
	{ "enum", KEYWORD_CLASS },
	{ "explicit", KEYWORD_CLASS },
	{ "export", KEYWORD_CLASS },
	{ "extern", KEYWORD_CLASS },
	{ "false", KEYWORD_CLASS },
	{ "float", KEYWORD_CLASS },
	{ "for", KEYWORD_CLASS },
	{ "friend", KEYWORD_CLASS },
	{ "goto", KEYWORD_CLASS },
	{ "if", KEYWORD_CLASS },
	{ "inline", KEYWORD_CLASS },
	{ "int", KEYWORD_CLASS },
	{ "long", KEYWORD_CLASS },
	{ "mutable", KEYWORD_CLASS },
	{ "namespace", KEYWORD_CLASS },
	{ "new", KEYWORD_CLASS },
	{ "operator", KEYWORD_CLASS },
	{ "private", KEYWORD_CLASS },
	{ "protected", KEYWORD_CLASS },
	{ "public", KEYWORD_CLASS },
	{ "register", KEYWORD_CLASS },
	{ "reinterpret_cast", KEYWORD_CLASS },
	{ "return", KEYWORD_CLASS },
	{ "short", KEYWORD_CLASS },
	{ "signed", KEYWORD_CLASS },
	{ "sizeof", KEYWORD_CLASS },
	{ "static", KEYWORD_CLASS },
	{ "static_cast", KEYWORD_CLASS },
	{ "struct", KEYWORD_CLASS },
	{ "switch", KEYWORD_CLASS },
	{ "template", KEYWORD_CLASS },
	{ "this", KEYWORD_CLASS },
	{ "throw", KEYWORD_CLASS },
	{ "true", KEYWORD_CLASS },
	{ "try", KEYWORD_CLASS },
	{ "typedef", KEYWORD_CLASS },
	{ "typeid", KEYWORD_CLASS },
	{ "typename", KEYWORD_CLASS },
	{ "union", KEYWORD_CLASS },
	{ "unsigned", KEYWORD_CLASS },
	{ "virtual", KEYWORD_CLASS },
	{ "void", KEYWORD_CLASS },
	{ "volatile", KEYWORD_CLASS },
	{ "wchar_t", KEYWORD_CLASS },
	{ "while", KEYWORD_CLASS },

/*
    { "INLINE", CUSTOM_CLASS },
    { "int8_t", CUSTOM_CLASS },
    { "uint8_t", CUSTOM_CLASS },
    { "int16_t", CUSTOM_CLASS },
    { "uint16_t", CUSTOM_CLASS },
    { "int32_t", CUSTOM_CLASS },
    { "uint32_t", CUSTOM_CLASS },
    { "int64_t", CUSTOM_CLASS },
    { "uint64_t", CUSTOM_CLASS },
    { "ARRAY_LENGTH", CUSTOM_CLASS },
*/
	{ nullptr, KEYWORD_CLASS }
};



/***************************************************************************
    PROTOTYPES
***************************************************************************/

// core output functions
static int recurse_dir(int srcrootlen, int dstrootlen, std::string &srcdir, std::string &dstdir, std::string &tempheader, std::string &tempfooter);
static int output_file(file_type type, int srcrootlen, int dstrootlen, std::string &srcfile, std::string &dstfile, bool link_to_file, std::string &tempheader, std::string &tempfooter);

// HTML helpers
static util::core_file::ptr create_file_and_output_header(std::string &filename, std::string &templatefile, std::string &path);
static void output_footer_and_close_file(util::core_file::ptr &&file, std::string &templatefile, std::string &path);

// path helpers
static std::string &normalized_subpath(std::string &dest, std::string &path, int start);
static void output_path_as_links(util::core_file &file, std::string &path, bool end_is_directory, bool link_to_file);
static bool find_include_file(std::string &srcincpath, int srcrootlen, int dstrootlen, std::string &srcfile, std::string &dstfile, std::string &filename);



/***************************************************************************
    MAIN
***************************************************************************/

/*-------------------------------------------------
    main - main entry point
-------------------------------------------------*/

void usage(const char *argv0)
{
	fprintf(stderr, "Usage:\n%s <srcroot> <destroot> <template.html> [-Iincpath [-Iincpath [...]]]\n", argv0);
	exit(1);
}

int main(int argc, char *argv[])
{
	// loop over arguments
	include_path **incpathhead = &incpaths;
	std::string srcdir, dstdir, tempfilename, tempheader, tempfooter;
	int unadorned = 0;
	for (int argnum = 1; argnum < argc; argnum++)
	{
		char *arg = argv[argnum];

		// include path?
		if (arg[0] == '-' && arg[1] == 'I')
		{
			*incpathhead = new include_path;
			if (*incpathhead != nullptr)
			{
				(*incpathhead)->next = nullptr;
				strreplacechr((*incpathhead)->path.assign(&arg[2]), '/', PATH_SEPARATOR[0]);
				incpathhead = &(*incpathhead)->next;
			}
		}

		// other parameter
		else if (arg[0] != '-' && unadorned == 0)
		{
			srcdir.assign(arg);
			unadorned++;
		}
		else if (arg[0] != '-' && unadorned == 1)
		{
			dstdir.assign(arg);
			unadorned++;
		}
		else if (arg[0] != '-' && unadorned == 2)
		{
			tempfilename.assign(arg);
			unadorned++;
		}
		else
			usage(argv[0]);
	}

	// make sure we got 3 parameters
	if (srcdir.length() == 0 || dstdir.length() == 0 || tempfilename.length() == 0)
		usage(argv[0]);

	// read the template file into an std::string
	uint32_t bufsize;
	void *buffer;
	if (util::core_file::load(tempfilename.c_str(), &buffer, bufsize) == osd_file::error::NONE)
	{
		tempheader.assign((const char *)buffer, bufsize);
		free(buffer);
	}

	// verify the template
	if (tempheader.length() == 0)
	{
		fprintf(stderr, "Unable to read template file\n");
		return 1;
	}
	int result = tempheader.find("<!--CONTENT-->");
	if (result == -1)
	{
		fprintf(stderr, "Template is missing a <!--CONTENT--> marker\n");
		return 1;
	}
	tempfooter.assign(tempheader).substr(result + 14, -1);
	tempheader.substr(0, result);

	// recurse over subdirectories
	return recurse_dir(srcdir.length(), dstdir.length(), srcdir, dstdir, tempheader, tempfooter);
}



/***************************************************************************
    CORE OUTPUT FUNCTIONS
***************************************************************************/

static int compare_list_entries(const void *p1, const void *p2)
{
	const list_entry *entry1 = *(const list_entry **)p1;
	const list_entry *entry2 = *(const list_entry **)p2;
	return entry1->name.compare(entry2->name);
}


/*-------------------------------------------------
    recurse_dir - recurse through a directory
-------------------------------------------------*/

static int recurse_dir(int srcrootlen, int dstrootlen, std::string &srcdir, std::string &dstdir, std::string &tempheader, std::string &tempfooter)
{
	static const osd::directory::entry::entry_type typelist[] = { osd::directory::entry::entry_type::DIR, osd::directory::entry::entry_type::FILE };

	// extract a normalized subpath
	std::string srcdir_subpath;
	normalized_subpath(srcdir_subpath, srcdir, srcrootlen + 1);

	// create an index file
	std::string indexname;
	indexname = string_format("%s%c%s", dstdir.c_str(), PATH_SEPARATOR[0], "index.html");
	util::core_file::ptr indexfile = create_file_and_output_header(indexname, tempheader, srcdir_subpath);

	// output the directory navigation
	indexfile->printf("<h3>Viewing Directory: ");
	output_path_as_links(*indexfile, srcdir_subpath, true, false);
	indexfile->printf("</h3>");

	// iterate first over directories, then over files
	int result = 0;
	for (int entindex = 0; entindex < ARRAY_LENGTH(typelist) && result == 0; entindex++)
	{
		auto entry_type = typelist[entindex];

		// open the directory and iterate through it
		auto dir = osd::directory::open(srcdir.c_str());
		if (dir == nullptr)
		{
			result = 1;
			break;
		}

		// build up the list of files
		const osd::directory::entry *entry;
		int found = 0;
		list_entry *list = nullptr;
		while ((entry = dir->read()) != nullptr)
			if (entry->type == entry_type && entry->name[0] != '.')
			{
				auto lentry = new list_entry;
				lentry->name.assign(entry->name);
				lentry->next = list;
				list = lentry;
				found++;
			}

		// close the directory
		dir.reset();

		// skip if nothing found
		if (found == 0)
			continue;

		// allocate memory for sorting
		auto listarray = new list_entry *[found];
		found = 0;
		for (list_entry *curlist = list; curlist != nullptr; curlist = curlist->next)
			listarray[found++] = curlist;

		// sort the list
		qsort(listarray, found, sizeof(listarray[0]), compare_list_entries);

		// rebuild the list
		list = nullptr;
		while (--found >= 0)
		{
			listarray[found]->next = list;
			list = listarray[found];
		}
		delete[] listarray;

		// iterate through each file
		for (list_entry *curlist = list; curlist != nullptr && result == 0; curlist = curlist->next)
		{
			// add a header
			if (curlist == list)
				indexfile->printf("\t<h2>%s</h2>\n\t<ul>\n", (entry_type == osd::directory::entry::entry_type::DIR) ? "Directories" : "Files");

			// build the source filename
			std::string srcfile;
			srcfile = string_format("%s%c%s", srcdir.c_str(), PATH_SEPARATOR[0], curlist->name.c_str());

			// if we have a file, output it
			std::string dstfile;
			if (entry_type == osd::directory::entry::entry_type::FILE)
			{
				// make sure we care, first
				file_type type = FILE_TYPE_INVALID;
				for (auto & elem : extension_lookup)
					if (core_filename_ends_with(curlist->name, elem.extension))
					{
						type = elem.type;
						break;
					}

				// if we got a valid file, process it
				if (type != FILE_TYPE_INVALID)
				{
					dstfile = string_format("%s%c%s.html", dstdir.c_str(), PATH_SEPARATOR[0], curlist->name.c_str());
					if (indexfile != nullptr)
						indexfile->printf("\t<li><a href=\"%s.html\">%s</a></li>\n", curlist->name.c_str(), curlist->name.c_str());
					result = output_file(type, srcrootlen, dstrootlen, srcfile, dstfile, srcdir.compare(dstdir) == 0, tempheader, tempfooter);
				}
			}

			// if we have a directory, recurse
			else
			{
				dstfile = string_format("%s%c%s", dstdir.c_str(), PATH_SEPARATOR[0], curlist->name.c_str());
				if (indexfile != nullptr)
					indexfile->printf("\t<li><a href=\"%s/index.html\">%s/</a></li>\n", curlist->name.c_str(), curlist->name.c_str());
				result = recurse_dir(srcrootlen, dstrootlen, srcfile, dstfile, tempheader, tempfooter);
			}
		}

		// close the list if we found some stuff
		if (list != nullptr)
			indexfile->printf("\t</ul>\n");

		// free all the allocated entries
		while (list != nullptr)
		{
			list_entry *next = list->next;
			delete list;
			list = next;
		}
	}

	if (indexfile != nullptr)
		output_footer_and_close_file(std::move(indexfile), tempfooter, srcdir_subpath);
	return result;
}


/*-------------------------------------------------
    output_file - output a file, converting to
    HTML
-------------------------------------------------*/

static int output_file(file_type type, int srcrootlen, int dstrootlen, std::string &srcfile, std::string &dstfile, bool link_to_file, std::string &tempheader, std::string &tempfooter)
{
	// extract a normalized subpath
	std::string srcfile_subpath;
	normalized_subpath(srcfile_subpath, srcfile, srcrootlen + 1);

	fprintf(stderr, "Processing %s\n", srcfile_subpath.c_str());

	// set some defaults
	bool color_quotes = false;
	const char *comment_start = "";
	const char *comment_start_esc = "";
	const char *comment_end = "";
	const char *comment_end_esc = "";
	const char *comment_inline = "";
	const char *comment_inline_esc = "";
	const char *token_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_#";
	const token_entry *token_table = dummy_token_table;

	// based on the file type, set the comment info
	switch (type)
	{
		case FILE_TYPE_C:
			color_quotes = true;
			comment_start = comment_start_esc = "/*";
			comment_end = comment_end_esc = "*/";
			comment_inline = comment_inline_esc = "//";
			token_table = c_token_table;
			break;

		case FILE_TYPE_MAKE:
			color_quotes = true;
			comment_inline = comment_inline_esc = "#";
			break;

		case FILE_TYPE_XML:
			color_quotes = true;
			comment_start = "<!--";
			comment_start_esc = "&lt;!--";
			comment_end = "-->";
			comment_end_esc = "--&gt;";
			break;

		default:
		case FILE_TYPE_TEXT:
			break;
	}

	// make the token lookup table
	bool is_token[256];
	memset(is_token, 0, sizeof(is_token));
	for (int toknum = 0; token_chars[toknum] != 0; toknum++)
		is_token[(uint8_t)token_chars[toknum]] = true;

	// open the source file
	util::core_file::ptr src;
	if (util::core_file::open(srcfile, OPEN_FLAG_READ, src) != osd_file::error::NONE)
	{
		fprintf(stderr, "Unable to read file '%s'\n", srcfile.c_str());
		return 1;
	}

	// open the output file
	util::core_file::ptr dst = create_file_and_output_header(dstfile, tempheader, srcfile_subpath);
	if (dst == nullptr)
	{
		fprintf(stderr, "Unable to write file '%s'\n", dstfile.c_str());
		return 1;
	}

	// output the directory navigation
	dst->printf("<h3>Viewing File: ");
	output_path_as_links(*dst, srcfile_subpath, false, link_to_file);
	dst->printf("</h3>");

	// start with some tags
	dst->printf("\t<pre class=\"source\">\n");

	// iterate over lines in the source file
	int linenum = 1;
	bool in_comment = false;
	char srcline[4096];
	std::ostringstream dstline;
	while (src->gets(srcline, ARRAY_LENGTH(srcline)) != nullptr)
	{
		// start with the line number
		dstline.str("");
		util::stream_format(dstline, "<span class=\"linenum\">%5d</span>&nbsp;&nbsp;", linenum++);

		// iterate over characters in the source line
		bool escape = false;
		bool in_quotes = false;
		bool in_inline_comment = false;
		bool last_token_was_include = false;
		bool last_was_token = false;
		bool quotes_are_linked = false;
		uint8_t curquote = 0;
		int curcol = 0;
		for (char *srcptr = srcline; *srcptr != 0; )
		{
			uint8_t ch = *srcptr++;

			// track whether or not we are within an extended (C-style) comment
			if (!in_quotes && !in_inline_comment)
			{
				if (!in_comment && ch == comment_start[0] && strncmp(srcptr - 1, comment_start, strlen(comment_start)) == 0)
				{
					util::stream_format(dstline, "<span class=\"comment\">%s", comment_start_esc);
					curcol += strlen(comment_start);
					srcptr += strlen(comment_start) - 1;
					ch = 0;
					in_comment = true;
				}
				else if (in_comment && ch == comment_end[0] && strncmp(srcptr - 1, comment_end, strlen(comment_end)) == 0)
				{
					util::stream_format(dstline, "%s</span>", comment_end_esc);
					curcol += strlen(comment_end);
					srcptr += strlen(comment_end) - 1;
					ch = 0;
					in_comment = false;
				}
			}

			// track whether or not we are within an inline (C++-style) comment
			if (!in_quotes && !in_comment && !in_inline_comment && ch == comment_inline[0] && strncmp(srcptr - 1, comment_inline, strlen(comment_inline)) == 0)
			{
				util::stream_format(dstline, "<span class=\"comment\">%s", comment_inline_esc);
				curcol += strlen(comment_inline);
				srcptr += strlen(comment_inline) - 1;
				ch = 0;
				in_inline_comment = true;
			}

			// if this is the start of a new token, see if we want to color it
			if (!in_quotes && !in_comment && !in_inline_comment && !last_was_token && is_token[ch])
			{
				const token_entry *curtoken;
				char *temp = srcptr;
				int toklength;

				// find the end of the token
				while (*temp != 0 && is_token[(uint8_t)*temp])
					temp++;
				toklength = temp - (srcptr - 1);

				// scan the token table
				last_token_was_include = false;
				for (curtoken = token_table; curtoken->token != nullptr; curtoken++)
					if (strncmp(srcptr - 1, curtoken->token, toklength) == 0 && strlen(curtoken->token) == toklength)
					{
						util::stream_format(dstline, "<span class=\"%s\">%s</span>", curtoken->color, curtoken->token);
						curcol += strlen(curtoken->token);
						srcptr += strlen(curtoken->token) - 1;
						ch = 0;

						// look for include tokens specially
						if (type == FILE_TYPE_C && strcmp(curtoken->token, "#include") == 0)
							last_token_was_include = true;
						break;
					}
			}
			last_was_token = is_token[ch];

			// if we hit a tab, expand it
			if (ch == 0x09)
			{
				// compute how many spaces
				int spaces = 4 - curcol % 4;
				while (spaces--)
				{
					dstline.put(' ');
					curcol++;
				}
			}

			// otherwise, copy the source character
			else if (ch != 0x0a && ch != 0x0d && ch != 0)
			{
				// track opening quotes
				if (!in_comment && !in_inline_comment && !in_quotes && (ch == '"' || ch == '\''))
				{
					if (color_quotes)
						util::stream_format(dstline, "<span class=\"string\">%c", ch);
					else
						dstline.put(ch);
					in_quotes = true;
					curquote = ch;

					// handle includes
					if (last_token_was_include)
					{
						char *endquote = strchr(srcptr, ch);
						if (endquote != nullptr)
						{
							std::string filename(srcptr, endquote - srcptr);
							std::string target;
							if (find_include_file(target, srcrootlen, dstrootlen, srcfile, dstfile, filename))
							{
								util::stream_format(dstline, "<a href=\"%s\">", target.c_str());
								quotes_are_linked = true;
							}
						}
					}
				}

				// track closing quotes
				else if (!in_comment && !in_inline_comment && in_quotes && (ch == curquote) && !escape)
				{
					if (quotes_are_linked)
						dstline << "</a>";
					if (color_quotes)
						util::stream_format(dstline, "%c</span>", ch);
					else
						dstline.put(ch);
					in_quotes = false;
					curquote = 0;
					quotes_are_linked = false;
				}

				// else just output the current character
				else if (ch == '&')
					dstline << "&amp;";
				else if (ch == '<')
					dstline << "&lt;";
				else if (ch == '>')
					dstline << "&gt;";
				else
					dstline.put(ch);
				curcol++;
			}

			// Update escape state
			if (in_quotes)
				escape = (ch == '\\' && type == FILE_TYPE_C) ? !escape : false;
		}

		// finish inline comments
		if (in_inline_comment)
		{
			dstline << "</span>";
			in_inline_comment = false;
		}

		// append a break and move on
		dstline.put('\n');
		dst->puts(dstline.str().c_str());
	}

	// close tags
	dst->printf("\t</pre>\n");

	// close the file
	output_footer_and_close_file(std::move(dst), tempfooter, srcfile_subpath);
	return 0;
}



/***************************************************************************
    HTML OUTPUT HELPERS
***************************************************************************/

/*-------------------------------------------------
    create_file_and_output_header - create a new
    HTML file with a standard header
-------------------------------------------------*/

static util::core_file::ptr create_file_and_output_header(std::string &filename, std::string &templatefile, std::string &path)
{
	// create the indexfile
	util::core_file::ptr file;
	if (util::core_file::open(filename, OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS | OPEN_FLAG_NO_BOM, file) != osd_file::error::NONE)
		return util::core_file::ptr();

	// print a header
	std::string modified(templatefile);
	strreplace(modified, "<!--PATH-->", path.c_str());
	file->write(modified.c_str(), modified.length());

	// return the file
	return file;
}


/*-------------------------------------------------
    output_footer_and_close_file - write a
    standard footer to an HTML file and close it
-------------------------------------------------*/

static void output_footer_and_close_file(util::core_file::ptr &&file, std::string &templatefile, std::string &path)
{
	std::string modified(templatefile);
	strreplace(modified, "<!--PATH-->", path.c_str());
	file->write(modified.c_str(), modified.length());
	file.reset();
}



/***************************************************************************
    HTML OUTPUT HELPERS
***************************************************************************/

/*-------------------------------------------------
    normalized_subpath - normalize a path to
    forward slashes and extract a subpath
-------------------------------------------------*/

static std::string &normalized_subpath(std::string &dest, std::string &path, int start)
{
	strreplacechr(dest.assign(path.substr(start, -1)),PATH_SEPARATOR[0], '/');
	return dest;
}


/*-------------------------------------------------
    output_path_as_links - output a path as a
    series of links
-------------------------------------------------*/

static void output_path_as_links(util::core_file &file, std::string &path, bool end_is_directory, bool link_to_file)
{
	// first count how deep we are
	int srcdepth = 0;
	for (int slashindex = path.find_first_of('/'); slashindex != -1; slashindex = path.find_first_of('/', slashindex + 1))
		srcdepth++;
	if (end_is_directory)
		srcdepth++;

	// output a link to the root
	file.printf("<a href=\"");
	for (int depth = 0; depth < srcdepth; depth++)
		file.printf("../");
	file.printf("index.html\">&lt;root&gt;</a>/");

	// now output links to each path up the chain
	int curdepth = 0;
	int lastslash = 0;
	for (int slashindex = path.find_first_of('/', lastslash); slashindex != -1; slashindex = path.find_first_of('/', lastslash))
	{
		std::string substr(path, lastslash, slashindex - lastslash);

		curdepth++;
		file.printf("<a href=\"");
		for (int depth = curdepth; depth < srcdepth; depth++)
			file.printf("../");
		file.printf("index.html\">%s</a>/", substr.c_str());

		lastslash = slashindex + 1;
	}

	// and a final link to the current directory
	std::string substr(path, lastslash, -1);
	if (end_is_directory)
		file.printf("<a href=\"index.html\">%s</a>", substr.c_str());
	else if (link_to_file)
		file.printf("<a href=\"%s\">%s</a>", substr.c_str(), substr.c_str());
	else
		file.printf("<a href=\"%s.html\">%s</a>", substr.c_str(), substr.c_str());
}


/*-------------------------------------------------
    find_include_file - find an include file
-------------------------------------------------*/

static bool find_include_file(std::string &srcincpath, int srcrootlen, int dstrootlen, std::string &srcfile, std::string &dstfile, std::string &filename)
{
	// iterate over include paths and find the file
	for (include_path *curpath = incpaths; curpath != nullptr; curpath = curpath->next)
	{
		// a '.' include path is specially treated
		if (curpath->path.compare(".") == 0)
			srcincpath.assign(srcfile.substr(0, srcfile.find_last_of(PATH_SEPARATOR[0])));
		else
			srcincpath.assign(srcfile.substr(0, srcrootlen + 1)).append(curpath->path);

		// append the filename piecemeal to account for directories
		int lastsepindex = 0;
		int sepindex;
		while ((sepindex = filename.find_first_of('/', lastsepindex)) != -1)
		{
			// handle .. by removing a chunk from the incpath
			std::string pathpart(filename, lastsepindex, sepindex - lastsepindex);
			if (pathpart.compare("..")==0)
			{
				sepindex = srcincpath.find_last_of(PATH_SEPARATOR[0]);
				if (sepindex != -1)
					srcincpath.substr(0, sepindex);
			}

			// otherwise, append a path separator and the pathpart
			else
				srcincpath.append(PATH_SEPARATOR).append(pathpart);

			// advance past the previous index
			lastsepindex = sepindex + 1;
		}

		// now append the filename
		srcincpath.append(PATH_SEPARATOR).append(filename.substr(lastsepindex, -1));

		// see if we can open it
		util::core_file::ptr testfile;
		if (util::core_file::open(srcincpath, OPEN_FLAG_READ, testfile) == osd_file::error::NONE)
		{
			// close the file
			testfile.reset();

			// find the longest matching directory substring between the include and source file
			lastsepindex = 0;
			while ((sepindex = srcincpath.find_first_of(PATH_SEPARATOR[0], lastsepindex)) != -1)
			{
				// get substrings up to the current directory
				std::string tempfile(srcfile, 0, sepindex);
				std::string tempinc(srcincpath, 0, sepindex);

				// if we don't match, stop
				if (tempfile.compare(tempinc)!=0)
					break;
				lastsepindex = sepindex + 1;
			}

			// chop off the common parts of the paths
			std::string tempfile(srcfile, lastsepindex, -1);
			srcincpath = srcincpath.substr(lastsepindex, -1);
			strreplacechr(srcincpath, PATH_SEPARATOR[0], '/');

			// for each directory left in the filename, we need to prepend a "../"
			while ((sepindex = tempfile.find_first_of(PATH_SEPARATOR[0])) != -1)
			{
				tempfile.substr(sepindex + 1, -1);
				srcincpath.insert(0, "../");
			}
			srcincpath.append(".html");

			// free the strings and return the include path
			return true;
		}
	}
	return false;
}