HTML utility code.


§1. Keeping track of which images are referred to in the HTML we generate:

dictionary *image_usages = NULL;  how many times, if at all, this image has been used

typedef struct image_usage {
    struct text_stream *leafname;
    int usage_count;
    struct filename *resolved_to;
    CLASS_DEFINITION
} image_usage;

typedef struct image_source {
    struct pathname *src;  a location from which images can be taken
    CLASS_DEFINITION
} image_source;

§2. We can either generate entirely fresh HTML pages, or we can inject material into a division inside a copy of an existing page: this is called "top-and-tailing".

int first_request_for_tt = TRUE;  will the next request be the first?
text_stream *DSET_main_tail_matter = NULL;
text_stream *DSET_main_top_matter = NULL;
text_stream *DSET_section_tail_matter = NULL;
text_stream *DSET_section_top_matter = NULL;

§3. Images. In the Inform application, a special http transport called inform: is used for references to the in-app documentation, so an image called orange.png will be found at the URL inform:/orange.png.

Otherwise, of course, we need to make an images folder alongside the HTML we produce. It will be called images, so relative to the HTML, the URL would then be images/orange.png.

However, it's also possible that the book's instructions tell us to use images from an already-existing location instead.

Note that each time the URL of an image is looked up, we make a note of it, so that we can keep track of which images we actually need.

void HTMLUtilities::image_URL(OUTPUT_STREAM, text_stream *leafname) {
    if (image_usages == NULL) image_usages = Dictionaries::new(100, FALSE);
    image_usage *iu = NULL;
    if (Dictionaries::find(image_usages, leafname)) {
        iu = Dictionaries::read_value(image_usages, leafname);
    } else {
        Dictionaries::create(image_usages, leafname);
        iu = CREATE(image_usage);
        iu->usage_count = 0;
        iu->leafname = Str::duplicate(leafname);
        Dictionaries::write_value(image_usages, leafname, iu);
    }
    iu->usage_count++;
    filename *F = Filenames::in(indoc_settings->images_path, leafname);
    if (indoc_settings->html_for_Inform_application == 1) WRITE("inform:/%/f", F);
    else {
        if (indoc_settings->images_copy)
            F = Filenames::in(Pathnames::from_text(I"images"), leafname);
        WRITE("%/f", F);
    }
    iu->resolved_to = F;
}

§4. For ebook use only:

void HTMLUtilities::note_images(void) {
    image_usage *iu;
    LOOP_OVER(iu, image_usage)
        Epub::note_image(indoc_settings->ebook, iu->resolved_to);
}

§5. Suppose we are indeed copying images into place: we have to get them from somewhere. The somewheres are folders called "image sources", and this routine adds one:

void HTMLUtilities::add_image_source(pathname *path) {
    image_source *is = CREATE(image_source);
    is->src = path;
}

§6. During the copying process, we look for each image, say orange.png, in each of these image sources, starting from the most recently added and working backwards. As soon as we find a file of that name, we copy it over.

void HTMLUtilities::copy_images(void) {
    if (indoc_settings->images_copy) {
        pathname *I = Pathnames::down(indoc_settings->destination, I"images");
        Pathnames::create_in_file_system(I);
        image_usage *iu;
        LOOP_OVER(iu, image_usage) {
            int found = FALSE;
            image_source *is;
            LOOP_BACKWARDS_OVER(is, image_source) {
                filename *F = Filenames::in(is->src, iu->leafname);
                if (TextFiles::exists(F)) {
                    found = TRUE;
                    Shell::copy(F, I, "-f");
                    break;
                }
            }
            if (found == FALSE) { Errors::with_text("unable to find the image %S", iu->leafname); }
        }
    }
}

§7. HTML file structure.

void HTMLUtilities::begin_file(OUTPUT_STREAM, volume *V) {
    HTML::declare_as_HTML(OUT, indoc_settings->XHTML);
    filename *CSS = NULL;
    if ((V) && (Str::len(V->vol_CSS_leafname) > 0))
        CSS = Filenames::from_text(V->vol_CSS_leafname);
    HTML::begin_head(OUT, CSS);
    HTML::comment(OUT, I"Generated by indoc");
}

void HTMLUtilities::write_title(OUTPUT_STREAM, text_stream *title) {
    WRITE("<title>%S</title>\n", title);
}

§8. Here's text within a link:

void HTMLUtilities::general_link(OUTPUT_STREAM, text_stream *cl, text_stream *to, text_stream *text) {
    HTML::begin_link_with_class(OUT, cl, to);
    WRITE("%S", text);
    HTML::end_link(OUT);
}

§9. And a link with both text and a title (tooltip) tag:

void HTMLUtilities::titled_link(OUTPUT_STREAM, text_stream *cl, text_stream *to, text_stream *ti, text_stream *text) {
    HTML::begin_link_with_class_title(OUT, cl, to, ti);
    WRITE("%S", text);
    HTML::end_link(OUT);
}

§10. And a standardised paragraph which is itself a link:

void HTMLUtilities::textual_link(OUTPUT_STREAM, text_stream *to, text_stream *text) {
    HTML_OPEN("p");
    HTMLUtilities::general_link(OUT, I"standardlink", to, text);
    HTML_CLOSE("p");
}

§11. Miscellaneous HTML. Horizontal ruled lines.

void HTMLUtilities::ruled_line(OUTPUT_STREAM) {
    if (indoc_settings->format == HTML_FORMAT) HTML_TAG("hr")
    else WRITE("----------------------------------------------------------------------\n");
}

§12. Images.

void HTMLUtilities::image_element(OUTPUT_STREAM, text_stream *name) {
    TEMPORARY_TEXT(details)
    WRITE_TO(details, "alt=\"%S\" src=\"", name);
    HTMLUtilities::image_URL(details, name);
    WRITE_TO(details, "\"");
    HTML::tag(OUT, "img", details);
    DISCARD_TEXT(details)
}

void HTMLUtilities::image_element_scaled(OUTPUT_STREAM, text_stream *name, int xsize, int ysize) {
    TEMPORARY_TEXT(details)
    WRITE_TO(details, "alt=\"%S\" src=\"", name);
    HTMLUtilities::image_URL(details, name);
    WRITE_TO(details, "\" width=\"%d\" height=\"%d\"", xsize, ysize);
    HTML::tag(OUT, "img", details);
    DISCARD_TEXT(details)
}

void HTMLUtilities::image_with_id(OUTPUT_STREAM, text_stream *name, text_stream *id) {
    TEMPORARY_TEXT(details)
    WRITE_TO(details, "alt=\"%S\" src=\"", name);
    HTMLUtilities::image_URL(details, name);
    WRITE_TO(details, "\" id=\"%S\"", id);
    HTML::tag(OUT, "img", details);
    DISCARD_TEXT(details)
}

void HTMLUtilities::asterisk_image(OUTPUT_STREAM, text_stream *name) {
    TEMPORARY_TEXT(details)
    WRITE_TO(details, "class=\"asterisk\" alt=\"*\" src=\"");
    HTMLUtilities::image_URL(details, name);
    WRITE_TO(details, "\"");
    HTML::tag_sc(OUT, "img", details);
    DISCARD_TEXT(details)
}

§13. The "extra" functions here are for revealing or concealing page content when the user clicks a button. Each such piece of content is in its own uniquely-ID'd <div>, as follows:

void HTMLUtilities::extra_div_open(OUTPUT_STREAM, int id) {
    HTML_OPEN_WITH("div", "id=\"extra%d\" style=\"display: none;\"", id);
}

§14. And the following links provide the wiring for the buttons:

void HTMLUtilities::extra_link(OUTPUT_STREAM, int id) {
    TEMPORARY_TEXT(onclick)
    WRITE_TO(onclick, "showExtra('extra%d', 'plus%d'); return false;", id, id);
    HTML::begin_link_with_class_onclick(OUT, NULL, I"#", onclick);
    DISCARD_TEXT(onclick)
    TEMPORARY_TEXT(details)
    WRITE_TO(details, "alt=\"show\" id=\"plus%d\" src=\"", id);
    HTMLUtilities::extra_icon(details, I"extra");
    WRITE_TO(details, "\"");
    HTML::tag(OUT, "img", details);
    DISCARD_TEXT(details)
    HTML_CLOSE("a");
    WRITE("&#160;");
}

void HTMLUtilities::all_extras_link(OUTPUT_STREAM, text_stream *from) {
    WRITE("&#160;");
    TEMPORARY_TEXT(onclick)
    WRITE_TO(onclick, "showExtra%S(); return false;", from);
    HTML::begin_link_with_class_onclick(OUT, NULL, I"#", onclick);
    DISCARD_TEXT(onclick)
    TEMPORARY_TEXT(details)
    WRITE_TO(details, "alt=\"show\" id=\"plus%S\" src=\"", from);
    HTMLUtilities::extra_icon(details, I"extrab");
    WRITE_TO(details, "\"");
    HTML::tag(OUT, "img", details);
    DISCARD_TEXT(details)
    HTML_CLOSE("a");
}

§15. These links call some standard Javascript functions, thus:

void HTMLUtilities::write_javascript_for_buttons(OUTPUT_STREAM) {
    WRITE("    function showExtra(id, imid) {\n");
    WRITE("        if (document.getElementById(id).style.display == 'block') {\n");
    WRITE("            document.getElementById(id).style.display = 'none';\n");
    WRITE("            document.getElementById(imid).src = '");
        HTMLUtilities::extra_icon(OUT, I"extra");
        WRITE("';\n");
    WRITE("        } else {\n");
    WRITE("            document.getElementById(id).style.display = 'block';\n");
    WRITE("            document.getElementById(imid).src = '");
        HTMLUtilities::extra_icon(OUT, I"extraclose");
        WRITE("';\n");
    WRITE("        }\n");
    WRITE("    }\n");
    WRITE("    function onLoaded() {\n");
    WRITE("        if (window.location.hash) {\n");
    WRITE("            var hash = window.location.hash.substring(2);\n");
    WRITE("            if (hash.search(\"_\") >= 0) {\n");
    WRITE("                var res = hash.split(\"_\");\n");
    WRITE("                showExample(\"example\"+res[1]);\n");
    WRITE("            } else {\n");
    WRITE("                showExample(\"example\"+hash);\n");
    WRITE("            }\n");
    WRITE("        }\n");
    WRITE("    }\n");
    WRITE("    window.onload=onLoaded;\n");
    WRITE("    function showExample(id) {\n");
    WRITE("        if (document.getElementById(id).style.display == 'block') {\n");
    WRITE("            document.getElementById(id).style.display = 'none';\n");
    WRITE("        } else {\n");
    WRITE("            document.getElementById(id).style.display = 'block';\n");
    WRITE("        }\n");
    WRITE("    }\n");
    WRITE("    function openExtra(id, imid) {\n");
    WRITE("        document.getElementById(id).style.display = 'block';\n");
    WRITE("        document.getElementById(imid).src = '");
        HTMLUtilities::extra_icon(OUT, I"extraclose");
        WRITE("';\n");
    WRITE("    }\n");
    WRITE("    function closeExtra(id, imid) {\n");
    WRITE("        document.getElementById(id).style.display = 'none';\n");
    WRITE("        document.getElementById(imid).src = '");
        HTMLUtilities::extra_icon(OUT, I"extra");
        WRITE("';\n");
    WRITE("    }\n");
}

§16. Where:

void HTMLUtilities::extra_icon(OUTPUT_STREAM, text_stream *name) {
    TEMPORARY_TEXT(img)
    WRITE_TO(img, "%S.png", name);
    HTMLUtilities::image_URL(OUT, img);
    DISCARD_TEXT(img)
}

§17. And the following compiles a set of specific functions for numbered buttons, used on the Contents page:

void HTMLUtilities::write_javascript_for_contents_buttons(OUTPUT_STREAM) {
    volume *V;
    LOOP_OVER(V, volume) {
        WRITE("    function showExtra%S() {\n", V->vol_abbrev);
        WRITE("        if (document.getElementById('plus%S').src.indexOf('", V->vol_abbrev);
            HTMLUtilities::extra_icon(OUT, I"extrab");
            WRITE("') >= 0) {\n");
        for (int i=0; i<V->vol_chapter_count; i++) {
            int bn = (V->allocation_id)*1000+i;
            WRITE("            openExtra('extra%d', 'plus%d');\n", bn, bn);
        }
        WRITE("            document.getElementById('plus%S').src = '", V->vol_abbrev);
            HTMLUtilities::extra_icon(OUT, I"extracloseb");
            WRITE("';\n");
        WRITE("        } else {\n");
        for (int i=0; i<V->vol_chapter_count; i++) {
            int bn = (V->allocation_id)*1000+i;
            WRITE("            closeExtra('extra%d', 'plus%d');\n", bn, bn);
        }
        WRITE("            document.getElementById('plus%S').src = '", V->vol_abbrev);
            HTMLUtilities::extra_icon(OUT, I"extrab");
            WRITE("';\n");
        WRITE("        }\n");
        WRITE(" }\n");
    }
}

§18. JavaScript to handle the paste icon works differently on different platforms.

void HTMLUtilities::paste_script(OUTPUT_STREAM, text_stream *content, int code_num) {
    HTML::open_javascript(OUT, FALSE);
    WRITE("function pasteCode");
    if (Str::len(content) == 0) WRITE("(code");
    else WRITE("%d(code", code_num);
    WRITE(") {\n");

    WRITE("    var myProject = window.Project;\n\n");
    WRITE("    myProject.selectView('source');\n");
    WRITE("    myProject.pasteCode(");
    if (Str::len(content) == 0) { WRITE("code"); }
    else WRITE("'%S'", content);
    WRITE(");\n}\n");
    HTML::close_javascript(OUT);
}

void HTMLUtilities::create_script(OUTPUT_STREAM, text_stream *content, int code_num, text_stream *titling) {
    HTML::open_javascript(OUT, FALSE);
    WRITE("function createNewProject");
    if (Str::len(content) == 0) WRITE("(code");
    else WRITE("%d(code", code_num);
    WRITE(", title) {\n");

    WRITE("    var myProject = window.Project;\n\n");
    WRITE("    myProject.createNewProject(");
    if (Str::len(content) == 0) WRITE("title");
    else WRITE("'%S'", titling);
    WRITE(", ");
    if (Str::len(content) == 0) WRITE("code");
    else WRITE("'%S'", content);
    WRITE(");\n}\n");
    HTML::close_javascript(OUT);
}

§19. Definitions. Some nice little boxes for syntax definitions in computery manuals:

int no_definition_anchors = 0;

void HTMLUtilities::definition_box(OUTPUT_STREAM, text_stream *defn, text_stream *sigil, volume *V, section *S) {
    if (indoc_settings->format == HTML_FORMAT) {
        TEMPORARY_TEXT(anchor)
        WRITE_TO(anchor, "defn%d", no_definition_anchors++);

        TEMPORARY_TEXT(comment)
        WRITE_TO(comment, "START PHRASE \"%S\"", anchor);
        HTML::comment(OUT, comment);
        DISCARD_TEXT(comment)

        HTML_OPEN_WITH("div", "class=\"definition\"");
        HTML::anchor(OUT, anchor);
        HTMLUtilities::defn_unpack(OUT, defn, V, S, anchor);
        WRITE("\n");
        DISCARD_TEXT(anchor)

        HTML::comment(OUT, I"END PHRASE");

        TEMPORARY_TEXT(defnhead)
        WRITE_TO(defnhead, "definition of %S", sigil);
        HTML::comment(OUT, defnhead);
        DISCARD_TEXT(defnhead)
    } else {
        WRITE("PHRASE: %S\n", defn);
    }
}

void HTMLUtilities::end_definition_box(OUTPUT_STREAM) {
    if (indoc_settings->format == HTML_FORMAT) {
        WRITE("\n");
        HTML::comment(OUT, I"end definition");
        HTML_CLOSE("div");
    } else WRITE("\n");
}

§20. Though these are in fact quite a lot of trouble:

void HTMLUtilities::defn_unpack(OUTPUT_STREAM, text_stream *given_defn, volume *V, section *S, text_stream *anchor) {
    match_results mr = Regexp::create_mr();
    Given alternates divided by an ampersand, recurse to unpack each in turn20.1;

    TEMPORARY_TEXT(defn)
    Str::copy(defn, given_defn);

    TEMPORARY_TEXT(index_as)
    Work out an index mark for this definition20.2;

    if (indoc_settings->inform_definitions_mode) {
        Rewrite definition as an HTML paragraph of class defnprototype20.3;
        Set the kind of the result of the phrase in italic, not bold20.4;
        Specially set and specially index a phrase to decide a condition20.5;
        Specially set and specially index a text substitution20.6;
        Tidy up the definition paragraph20.7;
        WRITE("%S", defn);
    } else {
        HTML_OPEN_WITH("span", "class=\"definitionterm\"");
        WRITE("%S", defn);
        HTML_CLOSE("span");
    }
    Regexp::dispose_of(&mr);
}

§20.1. Given alternates divided by an ampersand, recurse to unpack each in turn20.1 =

    if (Regexp::match(&mr, given_defn, U"(%c*?) & (%c*)")) {
        HTMLUtilities::defn_unpack(OUT, mr.exp[0], V, S, anchor);
        HTML_TAG("br");
        WRITE("<i>or:</i>&#160;&#160;&#160;");
        HTMLUtilities::defn_unpack(OUT, mr.exp[1], V, S, anchor);
        Regexp::dispose_of(&mr);
        return;
    }

§20.2. Work out an index mark for this definition20.2 =

    Str::copy(index_as, given_defn);
    if (indoc_settings->inform_definitions_mode) Regexp::replace(index_as, U" ...%c*", NULL, 0);
    Regexp::replace(index_as, U": *", NULL, 0);
    WRITE_TO(index_as, "=___=!definition");
    Indexes::mark_index_term(index_as, V, S, anchor, NULL, NULL, NULL);

§20.3. Rewrite definition as an HTML paragraph of class defnprototype20.3 =

    TEMPORARY_TEXT(proto)
    HTML::open(proto, "p", I"class='defnprototype'", __FILE__, __LINE__);
     given alternative wordings, put only the first in boldface
    Regexp::replace(defn, U"(%i+?)/(%C+)", U"%0</b>/%1<b>", REP_REPEATING);
    WRITE_TO(proto, "<b>%S</b>", defn);
    HTML::close(proto, "p", __FILE__, __LINE__);
    Str::copy(defn, proto);

§20.4. Set the kind of the result of the phrase in italic, not bold20.4 =

    if (Regexp::match(&mr, defn, U"(%c*) ... (%c*?)</b>")) {
        WRITE_TO(defn, "%S</b> ... <i>", mr.exp[0]);
        Regexp::replace(mr.exp[1], U"<b>", NULL, REP_REPEATING);
        Regexp::replace(mr.exp[1], U"</b>", NULL, REP_REPEATING);
        WRITE_TO(defn, "%S</i>", mr.exp[1]);
    }

§20.5. Specially set and specially index a phrase to decide a condition20.5 =

    if ((Regexp::match(&mr, defn, U"<b>if (%c*)")) &&
        (Regexp::match(NULL, defn, U"%c*a condition%c*"))) {
        WRITE_TO(defn, "<i>if</i> <b>%S", mr.exp[0]);
        Regexp::replace(index_as, U"if ", NULL, REP_ATSTART);
        text_stream *index_alph = NULL;
        if (Regexp::match(&mr, index_as, U"%(%c*?%) (%c*)")) index_alph = mr.exp[0];
        TEMPORARY_TEXT(term)
        WRITE_TO(term, "%S=___=!if-definition", index_as);
        Indexes::mark_index_term(term, V, S, anchor, NULL, NULL, index_alph);
        DISCARD_TEXT(term)
    }

§20.6. Specially set and specially index a text substitution20.6 =

    if (Regexp::match(&mr, defn, U"<b>say \"%[(%c*)%]\"</b>")) {
        WRITE_TO(defn, "say \"[<b>%S</b>]\"", mr.exp[0]);
        Regexp::replace(index_as, U"say ", NULL, REP_ATSTART);
        text_stream *index_alph = NULL;
        if (Regexp::match(&mr, index_as, U"%[%(?:%(%c*?%) %)?(%c*)%]")) index_alph = mr.exp[0];
        TEMPORARY_TEXT(term)
        WRITE_TO(term, "%S=___=!say-definition", index_as);
        Indexes::mark_index_term(term, V, S, anchor, NULL, NULL, index_alph);
        DISCARD_TEXT(term)
    }

§20.7. Tidy up the definition paragraph20.7 =

    Regexp::replace(defn, U"<b></b>", NULL, REP_REPEATING);
    Regexp::replace(defn, U"</b><b>", NULL, REP_REPEATING);

    TEMPORARY_TEXT(star)
    Str::copy(star, defn);
    Str::clear(defn);
    int bl = 0;
    while (Regexp::match(&mr, star, U"(%c)(%c*)")) {
        text_stream *ch = mr.exp[0]; Str::copy(star, mr.exp[1]);
        if (Str::eq(ch, I"(")) { if (bl == 0) WRITE_TO(defn, "</b>"); bl++; }
        WRITE_TO(defn, "%S", ch);
        if (Str::eq(ch, I")")) { bl--; if (bl == 0) WRITE_TO(defn, "<b>"); }
    }
    DISCARD_TEXT(star)

§21. Topping and tailing. If we're preparing HTML for use in a website, we probably want our documentation content to be just one <div> in a larger page whose design indoc knows nothing about. The following therefore lets us use an external file as the prototype, cutting it up into a head before the text [TEXT] and a tail afterwards.

void HTMLUtilities::read_top_and_tail_matter(filename *F, int main_flag) {
    tt_helper_state tths;
    tths.pretext = TRUE;
    if (main_flag) {
        DSET_main_top_matter = Str::new();
        DSET_main_tail_matter = Str::new();
        tths.top_text = DSET_main_top_matter;
        tths.tail_text = DSET_main_tail_matter;
    } else {
        DSET_section_top_matter = Str::new();
        DSET_section_tail_matter = Str::new();
        tths.top_text = DSET_section_top_matter;
        tths.tail_text = DSET_section_tail_matter;
    }
    TextFiles::read(F, FALSE, "can't read top and tail file",
        TRUE, HTMLUtilities::tt_helper, NULL, &tths);
}

§22.

typedef struct tt_helper_state {
    int pretext;
    text_stream *top_text;
    text_stream *tail_text;
} tt_helper_state;

void HTMLUtilities::tt_helper(text_stream *line, text_file_position *tfp, void *v_tths) {
    tt_helper_state *tths = (tt_helper_state *) v_tths;
    Str::trim_white_space_at_end(line);
    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, line, U"(%c*?)%[TEXT%](%c*)")) {
        WRITE_TO(tths->top_text, "%S", mr.exp[0]);
        HTML::begin_div_with_id(tths->top_text, "bodytext");
        HTML::end_div(tths->tail_text);
        WRITE_TO(tths->tail_text, "%S\n", mr.exp[1]);
        Regexp::dispose_of(&mr);
        tths->pretext = FALSE;
    } else {
        if (tths->pretext) WRITE_TO(tths->top_text, "%S\n", line);
        else WRITE_TO(tths->tail_text, "%S\n", line);
    }
}

§23. And this routine caches requests for tops or tails.

void HTMLUtilities::get_tt_matter(OUTPUT_STREAM, int top, int main) {
    if (first_request_for_tt) {
        if (indoc_settings->top_and_tail_sections)
            HTMLUtilities::read_top_and_tail_matter(indoc_settings->top_and_tail_sections, FALSE);
        if (indoc_settings->top_and_tail)
            HTMLUtilities::read_top_and_tail_matter(indoc_settings->top_and_tail, TRUE);
    }
    first_request_for_tt = FALSE;
    if ((top == 1) && (main == 1)) WRITE("%S", DSET_main_top_matter);
    if ((top == 1) && (main == 0)) WRITE("%S", DSET_main_tail_matter);
    if ((top == 0) && (main == 1)) WRITE("%S", DSET_section_top_matter);
    if ((top == 0) && (main == 0)) WRITE("%S", DSET_section_tail_matter);
}