To render a passage of extension documentation as HTML.


§1. Website renderer. We will make several HTML files, but only one at a time:

text_stream DOCF_struct;
text_stream *DOCF = NULL;

text_stream *DocumentationRenderer::open_subpage(pathname *P, text_stream *leaf) {
    if (P == NULL) return STDOUT;
    if (DOCF) internal_error("nested DC writes");
    filename *F = Filenames::in(P, leaf);
    SVEXPLAIN(2, "(writing documentation to file %f)\n", F);
    DOCF = &DOCF_struct;
    if (STREAM_OPEN_TO_FILE(DOCF, F, UTF8_ENC) == FALSE) {
        SVEXPLAIN(1, "(note: unable to write file %f)\n", F);
        DOCF = NULL;
        return NULL;  if we lack permissions, e.g., then write no documentation
    }
    return DOCF;
}

void DocumentationRenderer::close_subpage(void) {
    if (DOCF == NULL) internal_error("no DC page open");
    if (DOCF != STDOUT) STREAM_CLOSE(DOCF);
    DOCF = NULL;
}

§2. Our tree is turned into a tiny website, with a single index page for everything except the examples, and then up to 26 pages holding the content of examples A to Z.

void DocumentationRenderer::as_HTML(pathname *P, compiled_documentation *cd, text_stream *extras) {
    inbuild_nest *N = Supervisor::internal();
    if (N) {
        pathname *LP = Pathnames::down(Nests::get_location(N), I"PLs");
        Languages::set_default_directory(LP);
    }
    if (cd) {
        text_stream *OUT = DocumentationRenderer::open_subpage(P, cd->contents_URL_pattern);
        if (OUT) {
            markdown_item *md = NULL;
            if ((cd->markdown_content) && (cd->markdown_content->down) &&
                (cd->markdown_content->down->down) &&
                (cd->markdown_content->down->down->type == FILE_MIT)) {
                filename *F = Markdown::get_filename(cd->markdown_content->down->down);
                if (Str::eq(Filenames::get_leafname(F), cd->contents_URL_pattern))
                    md = cd->markdown_content->down->down;
            }
            if (cd->duplex_contents_page) {
                InformPages::header(OUT, I"Contents", JAVASCRIPT_FOR_STANDARD_PAGES_IRES, NULL);
                Manuals::duplex_contents_page(OUT, cd);
            } else {
                DocumentationRenderer::render_index_page(OUT, cd, md, extras);
            }
            DocumentationRenderer::close_subpage();
        }
        int vcount = 0;
        for (markdown_item *vol = cd->markdown_content->down; vol; vol = vol->next) {
            vcount++;
            text_stream *home_URL = DocumentationCompiler::home_URL_at_volume_item(vol);
            if (Str::ne(home_URL, cd->contents_URL_pattern)) {
                text_stream *OUT = DocumentationRenderer::open_subpage(P, home_URL);
                if (OUT) {
                    text_stream *volume_title = DocumentationCompiler::title_at_volume_item(cd, vol);
                    DocumentationRenderer::render_header(OUT, cd->title, volume_title, cd->within_extension);
                    HTML_OPEN("div");
                    HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
                    WRITE("contents");
                    HTML_CLOSE("p");
                    HTML_OPEN_WITH("ul", "class=\"extensioncontents\"");
                    DocumentationRenderer::render_toc_r(OUT, vol, 0, FALSE);
                    DocumentationRenderer::close_subpage();
                    HTML_CLOSE("ul");
                    HTML_CLOSE("div");
                    if ((vol->down) && (vol->down->type == FILE_MIT)) {
                        filename *F = Markdown::get_filename(vol->down);
                        if (Str::eq(Filenames::get_leafname(F), home_URL)) {
                            HTML_TAG("hr");
                            HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
                            WRITE("introduction");
                            HTML_CLOSE("p");
                            HTML_OPEN_WITH("div", "class=\"markdowncontent\"");
                            Markdown::render_extended(OUT, vol->down, InformFlavouredMarkdown::variation());
                            HTML_CLOSE("div");
                        }
                    }
                }
            }
            for (markdown_item *prev_md = NULL, *md = vol->down; md; prev_md = md, md = md->next)
                if (md->type == FILE_MIT) {
                    filename *F = Markdown::get_filename(md);
                    if (Str::ne(Filenames::get_leafname(F), home_URL)) {
                        text_stream *OUT = DocumentationRenderer::open_subpage(P, Filenames::get_leafname(F));
                        if (OUT) {
                            DocumentationRenderer::render_chapter_page(OUT, cd, prev_md, md, md->next, vcount);
                            DocumentationRenderer::close_subpage();
                        }
                    }
                }
        }
        IFM_example *egc;
        LOOP_OVER_LINKED_LIST(egc, IFM_example, cd->examples) {
            OUT = DocumentationRenderer::open_subpage(P, egc->URL);
            if (OUT) {
                DocumentationRenderer::render_example_page(OUT, cd, egc);
                DocumentationRenderer::close_subpage();
            }
        }
        cd_volume *primary = NULL;
        cd_volume *secondary = NULL;
        cd_volume *vol;
        LOOP_OVER_LINKED_LIST(vol, cd_volume, cd->volumes) {
            if (primary == NULL) primary = vol;
            else if (secondary == NULL) secondary = vol;
        }
        for (int ix=0; ix<NO_CD_INDEXES; ix++)
            if (cd->include_index[ix]) {
                text_stream *OUT = DocumentationRenderer::open_subpage(P, cd->index_URL_pattern[ix]);
                if (OUT) {
                    if (cd->duplex_contents_page) {
                        InformPages::header(OUT, I"Contents", JAVASCRIPT_FOR_STANDARD_PAGES_IRES, NULL);
                        Manuals::midnight_banner_for_indexes(OUT, cd, cd->index_title[ix]);
                    } else {
                        DocumentationRenderer::render_header(OUT, cd->title, cd->index_title[ix], cd->within_extension);
                    }
                    switch (ix) {
                        case GENERAL_INDEX:
                            Indexes::write_general_index(OUT, cd);
                            break;
                        case NUMERICAL_EG_INDEX:
                            Indexes::render_eg_index(OUT, (primary)?(primary->volume_item):NULL);
                            break;
                        case THEMATIC_EG_INDEX:
                            Indexes::render_eg_index(OUT, (secondary)?(secondary->volume_item):NULL);
                            break;
                        case ALPHABETICAL_EG_INDEX:
                            Indexes::write_example_index(OUT, cd);
                            break;
                    }
                    DocumentationRenderer::render_footer(OUT);
                    DocumentationRenderer::close_subpage();
                }
            }
        if (Str::len(cd->xrefs_file_pattern) > 0) {
            filename *XF = Filenames::in(P, cd->xrefs_file_pattern);
            text_stream XR_struct;
            text_stream *XR = &XR_struct;
            if (STREAM_OPEN_TO_FILE(XR, XF, UTF8_ENC)) {
                markdown_item *latest_file = NULL;
                DocumentationRenderer::list_tags(XR, cd->markdown_content, &latest_file);
                STREAM_CLOSE(XR);
            }
        }
    }
}

§3.

void DocumentationRenderer::list_tags(OUTPUT_STREAM, markdown_item *md,
    markdown_item **latest_file) {
    if (md->type == FILE_MIT) *latest_file = md;
    if ((md->type == HEADING_MIT) && (Markdown::get_heading_level(md) == 2)) {
        TEMPORARY_TEXT(sname)
        WRITE_TO(sname, "%S", md->stashed);
        if (Str::begins_with(sname, I"Section ")) Str::delete_n_characters(sname, 8);
        int N = DocumentationRenderer::list_actual_tags(OUT, md->down, 1);
        N += DocumentationRenderer::list_actual_tags(OUT, md->down, 2);
        if (N > 0) {
            WRITE("_ ");
            filename *F = Markdown::get_filename(*latest_file);
            Filenames::write_unextended_leafname(OUT, F);
            WRITE(" \"");
            for (int i=0; i<Str::len(sname); i++) {
                inchar32_t c = Str::get_at(sname, i);
                if (c == ':') break;
                PUT(c);
            }
            WRITE("\" \"");
            for (int i=0, colon_count=0; i<Str::len(sname); i++) {
                inchar32_t c = Str::get_at(sname, i);
                if ((c == ':') && (colon_count++ == 0)) { PUT('.'); }
                else { PUT(c); }
            }
            WRITE("\"\n");
        }
    }
    for (markdown_item *ch = md->down; ch; ch = ch->next)
        DocumentationRenderer::list_tags(OUT, ch, latest_file);
}

int DocumentationRenderer::list_actual_tags(OUTPUT_STREAM, markdown_item *md, int pass) {
    int t = 0;
    for (; md; md = md->next) {
        if (md->type == HEADING_MARKER_MIT) {
            int phrasal = FALSE;
            if ((Str::begins_with(md->stashed, I"ph_")) || (Str::begins_with(md->stashed, I"phs_")))
                phrasal = TRUE;
            if (((pass == 1) && (phrasal == FALSE)) ||
                ((pass == 2) && (phrasal == TRUE))) {
                t++;
                WRITE("%S ", md->stashed);
            }
        }
        if (md->down) t += DocumentationRenderer::list_actual_tags(OUT, md->down, pass);
    }
    return t;
}

void DocumentationRenderer::render_index_page(OUTPUT_STREAM, compiled_documentation *cd,
    markdown_item *md, text_stream *extras) {
    DocumentationRenderer::render_header(OUT, cd->title, NULL, cd->within_extension);
    if (cd->associated_extension) {
        DocumentationRenderer::render_extension_details(OUT, cd->associated_extension);
    }

    HTML_TAG("hr");
    if (DocumentationCompiler::scold(OUT, cd) == FALSE) {
        HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
        WRITE("documentation");
        HTML_CLOSE("p");
        if (cd->empty) {
            HTML_OPEN("p");
            InformFlavouredMarkdown::render_text(OUT, I"None is provided.");
            HTML_CLOSE("p");
        } else {
            if (LinkedLists::len(cd->volumes) == 1) {
                DocumentationRenderer::render_toc(OUT, cd);
            } else {
                HTML_OPEN("p");
                InformFlavouredMarkdown::render_text(OUT, I"The following manuals are provided:");
                HTML_CLOSE("p");
                HTML_OPEN("ul");
                cd_volume *vol;
                LOOP_OVER_LINKED_LIST(vol, cd_volume, cd->volumes) {
                    HTML_OPEN_WITH("li", "class=\"exco1\"");
                    HTML_OPEN_WITH("a", "style=\"text-decoration: none\" href=\"%S\"",
                        vol->home_URL);
                    InformFlavouredMarkdown::render_text(OUT, vol->title);
                    HTML_CLOSE("a");
                    HTML_CLOSE("li");
                }
                DocumentationRenderer::render_toc_indexes(OUT, cd);
                HTML_CLOSE("ul");
            }
            if (md) {
                HTML_TAG("hr");
                HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
                WRITE("introduction");
                HTML_CLOSE("p");
                HTML_OPEN_WITH("div", "class=\"markdowncontent\"");
                Markdown::render_extended(OUT, md, InformFlavouredMarkdown::variation());
                HTML_CLOSE("div");
            }
        }
        WRITE("%S", extras);
    }

    Enter the small print3.1;
    WRITE("These documentation pages are first generated when an extension is "
        "installed, and refreshed each time the project successfully translates.");
    Exit the small print3.2;
    DocumentationRenderer::render_footer(OUT);
}

§3.1. Enter the small print3.1 =

    HTML_TAG("hr")
    HTML_OPEN("p")
    HTML_OPEN("em");

§3.2. Exit the small print3.2 =

    HTML_CLOSE("em");
    HTML_CLOSE("p");

§4.

void DocumentationRenderer::render_example_page(OUTPUT_STREAM, compiled_documentation *cd,
    IFM_example *egc) {
    TEMPORARY_TEXT(title)
    WRITE_TO(title, "Example %d", egc->number);
    DocumentationRenderer::render_header(OUT, cd->title, title, cd->within_extension);
    DISCARD_TEXT(title)
    DocumentationRenderer::render_example(OUT, cd, egc);
    DocumentationRenderer::render_footer(OUT);
}

void DocumentationRenderer::render_chapter_page(OUTPUT_STREAM, compiled_documentation *cd,
    markdown_item *prev_file, markdown_item *file_marker, markdown_item *next_file,
    int vcount) {
    TEMPORARY_TEXT(title)
    DocumentationRenderer::file_title(title, file_marker);
    if (cd->duplex_contents_page) {
        InformPages::header(OUT, title, JAVASCRIPT_FOR_ONE_EXTENSION_IRES, NULL);
        Manuals::midnight_section_title(OUT, cd, Markdown::get_filename(prev_file),
            title, Markdown::get_filename(next_file));
        if (vcount == 1) {
            HTML_OPEN_WITH("div", "class=\"duplexleftpage\"");
        } else {
            HTML_OPEN_WITH("div", "class=\"duplexrightpage\"");
        }
    } else {
        DocumentationRenderer::render_header(OUT, cd->title, title, cd->within_extension);
    }
    DISCARD_TEXT(title)
    HTML_OPEN_WITH("div", "class=\"markdowncontent\"");
    Markdown::render_extended(OUT, file_marker, InformFlavouredMarkdown::variation());
    HTML_CLOSE("div");
    Enter the small print3.1;
    if (prev_file) {
        WRITE(" &bull; ");
        HTML_OPEN_WITH("a", "href=\"%f\"", Markdown::get_filename(prev_file));
        DocumentationRenderer::file_title(OUT, prev_file);
        HTML_CLOSE("a");
    }
    if (next_file) {
        WRITE(" &bull; ");
        HTML_OPEN_WITH("a", "href=\"%f\"", Markdown::get_filename(next_file));
        DocumentationRenderer::file_title(OUT, next_file);
        HTML_CLOSE("a");
    }
    Exit the small print3.2;
    if (cd->duplex_contents_page) {
        HTML_CLOSE("div");
    }
    DocumentationRenderer::render_footer(OUT);
}

void DocumentationRenderer::file_title(OUTPUT_STREAM, markdown_item *file_marker) {
    if ((file_marker->down) && (file_marker->down->type == HEADING_MIT))
        InformFlavouredMarkdown::render_text(OUT, file_marker->down->stashed);
    else
        WRITE("Preface");
}

§5. Each of these pages is equipped with the same Javascript and CSS.

void DocumentationRenderer::render_header(OUTPUT_STREAM, text_stream *title, text_stream *ptitle,
    inform_extension *within) {
    InformPages::header(OUT, title, JAVASCRIPT_FOR_ONE_EXTENSION_IRES, NULL);
    ExtensionWebsite::add_home_breadcrumb(NULL);
    if (within) {
        ExtensionWebsite::add_breadcrumb(within->as_copy->edition->work->title,
            I"../index.html");
    }
    ExtensionWebsite::add_breadcrumb(title, I"index.html");
    if (Str::len(ptitle) > 0) ExtensionWebsite::add_breadcrumb(ptitle, NULL);
    ExtensionWebsite::titling_and_navigation(OUT,
        I"Documentation provided by the extension author");
}

void DocumentationRenderer::render_footer(OUTPUT_STREAM) {
    InformPages::footer(OUT);
}

§6. This function is the only one which assumes our documentation comes from an extension.

void DocumentationRenderer::render_extension_details(OUTPUT_STREAM, inform_extension *E) {
    inbuild_edition *edition = E->as_copy->edition;
    inbuild_work *work = edition->work;

    HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
    WRITE("about this extension");
    HTML_CLOSE("p");

    HTML_OPEN("p");
    WRITE("This is ");
    Works::write_to_HTML_file(OUT, work, TRUE);
    semantic_version_number V = E->as_copy->edition->version;
    if (VersionNumbers::is_null(V)) WRITE(", which gives no version number");
    else WRITE(", version %v", &V);
    WRITE(".");
    HTML_CLOSE("p");

    if (Str::len(E->rubric_as_lexed) > 0) {
        HTML_OPEN("p");
        InformFlavouredMarkdown::render_text(OUT, E->rubric_as_lexed);
        HTML_CLOSE("p");
    }

    if (Str::len(E->extra_credit_as_lexed) > 0) {
        HTML_OPEN("p");
        HTML_OPEN("em");
        InformFlavouredMarkdown::render_text(OUT, E->extra_credit_as_lexed);
        HTML_CLOSE("em");
        HTML_CLOSE("p");
    }
    compatibility_specification *C = E->as_copy->edition->compatibility;
    if (Str::len(C->parsed_from) > 0) {
        HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
        WRITE("compatibility");
        HTML_CLOSE("p");
        HTML_OPEN("p");
        InformFlavouredMarkdown::render_text(OUT, C->parsed_from);
        HTML_CLOSE("p");
    }
}

§7. Now for the Table of Contents, which shows chapters, sections and examples in a hierarchical fashion.

void DocumentationRenderer::render_toc(OUTPUT_STREAM, compiled_documentation *cd) {
    HTML_OPEN("div");
    HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
    WRITE("contents");
    HTML_CLOSE("p");
    HTML_OPEN_WITH("ul", "class=\"extensioncontents\"");
    int include_examples = TRUE;
    if (cd->include_index[NUMERICAL_EG_INDEX]) include_examples = FALSE;
    if (LinkedLists::len(cd->volumes) > 1) {
        for (markdown_item *vol = cd->markdown_content->down; vol; vol = vol->next) {
            HTML_OPEN_WITH("li", "class=\"exco0\"");
            HTML_OPEN("b");
            WRITE("%S", vol->stashed);
            HTML_CLOSE("b");
            HTML_OPEN_WITH("ul", "class=\"extensioncontents\"");
            DocumentationRenderer::render_toc_r(OUT, vol, 0, include_examples);
            HTML_CLOSE("ul");
            HTML_CLOSE("li");
        }
    } else {
        DocumentationRenderer::render_toc_r(OUT, cd->markdown_content, 0, include_examples);
    }

    DocumentationRenderer::render_toc_indexes(OUT, cd);

    HTML_CLOSE("ul");
    HTML_CLOSE("div");
}

void DocumentationRenderer::render_toc_indexes(OUTPUT_STREAM, compiled_documentation *cd) {
    for (int ix=0; ix<NO_CD_INDEXES; ix++)
        if (cd->include_index[ix]) {
            HTML_OPEN_WITH("li", "class=\"exco1\"");
            HTML_OPEN("b");
            HTML_OPEN_WITH("a", "style=\"text-decoration: none\" href=\"%S\"",
                cd->index_URL_pattern[ix]);
            WRITE("%S", cd->index_title[ix]);
            HTML_CLOSE("a");
            HTML_CLOSE("b");
            HTML_CLOSE("li");
        }
}

void DocumentationRenderer::link_to(OUTPUT_STREAM, markdown_item *md) {
    if (md->type != HEADING_MIT) internal_error("not a heading");
    text_stream *ch = MarkdownVariations::URL_for_heading(md);
    HTML_OPEN_WITH("a", "style=\"text-decoration: none\" href=%S", ch);
}

void DocumentationRenderer::render_toc_r(OUTPUT_STREAM, markdown_item *md, int L,
    int include_examples) {
    if ((md->type == HEADING_MIT) && (Markdown::get_heading_level(md) <= 2)) {
        if (L > 0) {
            HTML_OPEN_WITH("li", "class=\"exco%d\"", Markdown::get_heading_level(md));
            HTML::begin_span(OUT, I"indexblack");
            HTML_OPEN("b");
            DocumentationRenderer::link_to(OUT, md);
            for (markdown_item *ch = md->down; ch; ch = ch->next)
                Markdown::render_extended(OUT, ch, InformFlavouredMarkdown::variation());
            HTML_CLOSE("a");
            HTML_CLOSE("b");
            HTML::end_span(OUT);
            HTML_CLOSE("li");
            WRITE("\n");
        }
    }
    if ((md->type == INFORM_EXAMPLE_HEADING_MIT) && (include_examples)) {
        HTML_OPEN_WITH("li", "class=\"exco%d\"", L);
        IFM_example *E = RETRIEVE_POINTER_IFM_example(md->user_state);
        TEMPORARY_TEXT(link)
        WRITE_TO(link, "style=\"text-decoration: none\" href=\"%S#eg%S\"",
            E->URL, E->insignia);
        HTML::begin_span(OUT, I"indexblack");
        HTML_OPEN_WITH("a", "%S", link);
        WRITE("Example %S &mdash; ", E->insignia);
        InformFlavouredMarkdown::render_text(OUT, E->name);
        HTML_CLOSE("a");
        HTML::end_span(OUT);
        DISCARD_TEXT(link)
        HTML_CLOSE("li");
        WRITE("\n");
    }
    for (markdown_item *ch = md->down; ch; ch = ch->next)
        DocumentationRenderer::render_toc_r(OUT, ch, L+1, include_examples);
}

void DocumentationRenderer::render_example(OUTPUT_STREAM, compiled_documentation *cd,
    IFM_example *egc) {
    markdown_item *alt_EN = egc->header;
    if (alt_EN == NULL) {
        WRITE("Example %d is missing", egc->number);
    } else {
        IFM_example *E = RETRIEVE_POINTER_IFM_example(alt_EN->user_state);
        HTML_OPEN_WITH("div", "class=\"markdowncontent\"");
        InformFlavouredMarkdown::render_example_heading(OUT, E, NULL);
        markdown_item *passage_node = alt_EN->down;
        while (passage_node) {
            Markdown::render_extended(OUT, passage_node,
                InformFlavouredMarkdown::variation());
            passage_node = passage_node->next;
        }
        HTML_CLOSE("div");
        Enter the small print3.1;
        if (egc->cue) {
            WRITE("This example is drawn from ");
            DocumentationRenderer::link_to(OUT, egc->cue);
            Markdown::render_extended(OUT, egc->cue->down,
                InformFlavouredMarkdown::variation());
            HTML_CLOSE("a");
        }
        Exit the small print3.2;
    }
}