To render problem messages either as plain text or HTML, and write them out to files.


§1. Buffering. Only one error text needs to be stored in memory at any one time, so we keep it in a single text stream:

    text_stream *PBUFF = NULL;

    void Problems::Buffer::clear(void) {
        if (PBUFF == NULL) PBUFF = Str::new();
        else Str::clear(PBUFF);
    }

The function Problems::Buffer::clear is used in 2/pl2 (§3.1, §9).

§2. Roughly speaking, text for problem messages comes from two possible sources: fairly short standard texts inside NI, and quotations (direct or indirect) from the source text. The latter are inserted by the routines in this section, and they are the ones we should be wary of, since bizarre input might cause absurdly long quotations to be made. A quotation involves copying text from word w1 to w2, so there are two dangers: copying a single very long word, or copying too many of them. We protect against the first by using the %<W escape, which truncates long literals. We protect against the second overflow hazard by limiting the amount of text we are prepared to quote from any sentence in one go:

    define QUOTATION_TOLERANCE_LIMIT 100
    void Problems::Buffer::copy_text_into_problem_buffer(wording W) {
        W = Wordings::truncate(W, QUOTATION_TOLERANCE_LIMIT);
        WRITE_TO(PBUFF, "%<W", W);
    }

The function Problems::Buffer::copy_text_into_problem_buffer is used in §4, 2/pl2 (§3.1, §11.1.1).

§3. Diverting source quotes.

    parse_node *redirected_sentence = NULL;
    parse_node *redirected_to_A = NULL, *redirected_to_B = NULL;
    void Problems::Buffer::redirect_problem_sentence(parse_node *from, parse_node *A, parse_node *B) {
        redirected_sentence = from; redirected_to_A = A; redirected_to_B = B;
    }

The function Problems::Buffer::redirect_problem_sentence appears nowhere else.

§4. A special escape sequence, marked by starting and finishing with a character which otherwise can never occur, is expanded to a source code link. If we used an asterisk to denote this, then a source reference is fed into HTMLFiles::char_out as the following stream of characters:

*source text*Source/story.ni*14*

(with SOURCE_REF_CHAR used in place of the asterisk).

But of course we don't use an asterisk as trigger — we use character F0. Arguably this is dodgy, since the character is legal in ISO Latin-1. But it is a lower-case Icelandic eth, which is not allowed in unquoted Inform 7 source text (because of being absent from the ZSCII character set).

    define SOURCE_REF_CHAR L'\xf0'
    define FORCE_NEW_PARA_CHAR L'\xd0'
    define PROTECTED_LT_CHAR L'\x01'
    define PROTECTED_GT_CHAR L'\x02'
    void Problems::Buffer::copy_source_reference_into_problem_buffer(wording W) {
        if (Wordings::empty(W)) { WRITE_TO(PBUFF, "<no text>"); return; }
        source_file *referred = Lexer::file_of_origin(Wordings::first_wn(W));
        TEMPORARY_TEXT(file);
        if (referred) {
            WRITE_TO(file, "%f", TextFromFiles::get_filename(referred));
            #ifdef INBUILD_MODULE
            pathname *proj = Projects::path(Inbuild::project());
            if (proj) {
                TEMPORARY_TEXT(project_prefix);
                WRITE_TO(project_prefix, "%p", proj);
                if (Str::prefix_eq(file, project_prefix, Str::len(project_prefix)))
                    Str::delete_n_characters(file, Str::len(project_prefix));
            }
            #endif
        } else {
            WRITE_TO(file, "(no file)");
        }
        WRITE_TO(PBUFF, "'");
        Problems::Buffer::copy_text_into_problem_buffer(W);
        text_stream *paraphrase = file;
        #ifdef INBUILD_MODULE
        paraphrase = I"source text";
        inform_extension *E = SourceFiles::get_extension_corresponding(referred);
        if (E) {
            inbuild_work *work = E->as_copy->edition->work;
            if ((work) && (Works::is_standard_rules(work)))
                paraphrase = I"the Standard Rules";
            else if ((work) && (Works::is_basic_inform(work)))
                paraphrase = I"Basic Inform";
            else
                paraphrase = file;
        }
        #endif
        WRITE_TO(PBUFF, "' %c%S%c%S%c%d%c",
            SOURCE_REF_CHAR, paraphrase,
            SOURCE_REF_CHAR, file,
            SOURCE_REF_CHAR, Wordings::location(W).line_number,
            SOURCE_REF_CHAR);
        if ((redirected_sentence) &&
            (redirected_to_A) &&
            (redirected_to_B) &&
            (Wordings::eq(ParseTree::get_text(redirected_sentence), W))) {
            WRITE_TO(PBUFF, " (which asserts that ");
            Problems::Buffer::copy_source_reference_into_problem_buffer(
                ParseTree::get_text(redirected_to_A));
            WRITE_TO(PBUFF, " is/are ");
            Problems::Buffer::copy_source_reference_into_problem_buffer(
                ParseTree::get_text(redirected_to_B));
            WRITE_TO(PBUFF, ")");
        }
        DISCARD_TEXT(file);
    }

The function Problems::Buffer::copy_source_reference_into_problem_buffer is used in 2/pl2 (§10, §11.1.1).

§5. Once the error message is fully constructed, we will want to output it to a file: in fact, by default it will go in three directions, to stderr, to the debugging log and of course to the error log. The main thing is to word-wrap it, since it is likely to be a paragraph-sized chunk of text, not a single line. The unprintable SOURCE_REF_CHAR and FORCE_NEW_PARA_CHAR are simply filtered out for plain text output: for HTML, they are dealt with elsewhere.

    int problem_count_at_last_in = 1;

    #ifndef PROBLEMS_HTML_EMITTER
    #define PROBLEMS_HTML_EMITTER PUT_TO
    #endif

    void Problems::Buffer::output_problem_buffer_to(OUTPUT_STREAM, int indentation) {
        int line_width = 0, html_flag = FALSE;
        if (OUT == problems_file) html_flag = TRUE;
        for (int k=0; k<indentation; k++) { WRITE("  "); line_width+=2; }
        for (int i=0, L=Str::len(PBUFF); i<L; i++) {
            int c = Str::get_at(PBUFF, i);
            <In HTML mode, convert drawing-your-attention arrows 5.1>;
            <In plain text mode, remove bold and italic HTML tags 5.2>;
            if ((html_flag == FALSE) && (c == SOURCE_REF_CHAR))
                <Issue plain text paraphrase of source reference 5.3>
            else <Output single character of problem message 5.4>;
        }
        if (html_flag) HTML_CLOSE("p")
        else WRITE("\n");
    }

The function Problems::Buffer::output_problem_buffer_to is used in §6.

§5.1. The plain text "may I draw your attention to the following paragraph" marker,

>--> Which looks like this.

is converted into a suitable CSS-styled HTML paragraph with hanging indentation. And similarly for >++>, used to mark continuations.

<In HTML mode, convert drawing-your-attention arrows 5.1> =

        if ((html_flag) && (Str::includes_wide_string_at(PBUFF, L">-->", i))) {
            if (problem_count > problem_count_at_last_in) {
                HTML_TAG("hr");
            }
            HTML_OPEN_WITH("p", "class=\"hang\"");
            WRITE("<b>Problem.</b> ");
            i+=3; continue;
        }
        if (Str::includes_wide_string_at(PBUFF, L">++>", i)) {
            if (html_flag) HTML_OPEN_WITH("p", "class=\"in2\"") else WRITE("  ");
            i+=3; continue;
        }
        if (Str::includes_wide_string_at(PBUFF, L">--->", i)) {
            if (html_flag) {
                HTML_CLOSE("p"); HTML_TAG("hr");
            }
            problem_count_at_last_in = problem_count+1;
            i+=4; continue;
        }
        if (Str::includes_wide_string_at(PBUFF, L">+++>", i)) {
            if (html_flag) HTML_OPEN_WITH("p", "halftightin3\"") else WRITE("  ");
            i+=4; continue;
        }
        if (Str::includes_wide_string_at(PBUFF, L">++++>", i)) {
            if (html_flag) HTML_OPEN_WITH("p", "class=\"tightin3\"") else WRITE("  ");
            i+=5; continue;
        }

This code is used in §5.

§5.2. The problem messages are put together (by Level 2 below) in a plain text way, but with a little formatting included: in particular, they contain HTML-style <i>, <b> and <span> tags, which the following code strips out when writing to plain text format.

<In plain text mode, remove bold and italic HTML tags 5.2> =

        if (html_flag == FALSE) {
            if (c == PROTECTED_LT_CHAR) {
                while ((i<L) && (Str::get_at(PBUFF, i) != PROTECTED_GT_CHAR)) i++;
                continue;
            }
            if ((c == '<') &&
                    ((Str::includes_wide_string_at_insensitive(PBUFF, L"<i>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<b>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<img>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<a>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<font>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<i ", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<b ", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<img ", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<a ", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<font ", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"<span ", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"</i>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"</b>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"</img>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"</a>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"</span>", i)) ||
                    (Str::includes_wide_string_at_insensitive(PBUFF, L"</font>", i)))) {
                while ((i<L) && (Str::get_at(PBUFF, i) != '>')) i++;
                continue;
            }
        }

This code is used in §5.

§5.3. Okay, so the format for a source reference here is:

        XparaphraseXfilenameXnumberX

e.g., Xmain textXsource/story.niX102, where X is the unprintable SOURCE_REF_CHAR. The counter i is at the first X, and we must now convert this to something fit for printing to stdout, finishing up with i pointing to the last X.

We always use the paraphrase, not the filename, on stdout because (i) that's slightly easier to understand for the user, but more importantly (ii) it makes the output the same on all platforms when only main text and Standard Rules are referred to, and that simplifies intest and the Test Suite quite a bit, because we don't have to worry about trivial differences between OS X and Windows caused by the slashes going the wrong way, and so on.

<Issue plain text paraphrase of source reference 5.3> =

        WRITE("("); line_width++;
        while (Str::get_at(PBUFF, ++i) != SOURCE_REF_CHAR) <Output single character of problem message 5.4>;
        while (Str::get_at(PBUFF, ++i) != SOURCE_REF_CHAR) ;
        WRITE(", line "); line_width += 7;
        while (Str::get_at(PBUFF, ++i) != SOURCE_REF_CHAR) <Output single character of problem message 5.4>;
        WRITE(")"); line_width++;

This code is used in §5.

§5.4. <Output single character of problem message 5.4> =

        c = Str::get_at(PBUFF, i);
        if (Characters::is_whitespace(c)) {     this starts a run of whitespace
            int l = i; while (Characters::is_whitespace(Str::get_at(PBUFF, l))) l++;
            if (Str::get_at(PBUFF, l) == 0) break;     omit any trailing spaces
            i = l - 1;     skip to final whitespace character of the run
            if (html_flag) PROBLEMS_HTML_EMITTER(OUT, ' ');
            else <In plain text mode, wrap the line or print a space as necessary 5.4.1>;
        } else {
            line_width++;
            if (c == PROTECTED_LT_CHAR) PUT('<');
            else if (c == PROTECTED_GT_CHAR) PUT('>');
            else if (html_flag) PROBLEMS_HTML_EMITTER(OUT, c);
            else if ((c != SOURCE_REF_CHAR) && (c != FORCE_NEW_PARA_CHAR)) WRITE("%c", c);
        }

This code is used in §5, §5.3 (twice).

§5.4.1. At this point, l is the position of the first non-whitespace character after the sequence of whitespace.

    define PROBLEM_WORD_WRAP_WIDTH 80

<In plain text mode, wrap the line or print a space as necessary 5.4.1> =

        int word_width = 0;
        while ((!Characters::is_whitespace(Str::get_at(PBUFF, l))) && (Str::get_at(PBUFF, l) != 0)
            && (Str::get_at(PBUFF, l) != SOURCE_REF_CHAR))
            l++, word_width++;
        if (line_width + word_width + 1 >= PROBLEM_WORD_WRAP_WIDTH) {
            WRITE("\n"); line_width = 0;
            for (l=0; l<indentation+1; l++) { line_width+=2; WRITE("  "); }
        } else {
            WRITE(" "); line_width++;
        }

This code is used in §5.4.

§6. The following handles the three-way distribution of problems, but also allows us to route individual messages to only one output of our choice by temporarily setting the probl variable: which is a convenience for informational messages such as appear in index files, for instance.

    void Problems::Buffer::redirect_problem_stream(text_stream *S) {
        probl = S;
    }

    int telemetry_recording = FALSE;

    void Problems::Buffer::output_problem_buffer(int indentation) {
        if (probl == NULL) {
            Problems::Buffer::output_problem_buffer_to(problems_file, indentation);
            WRITE_TO(problems_file, "\n");
            Problems::Buffer::output_problem_buffer_to(STDERR, indentation);
            STREAM_FLUSH(STDERR);
            WRITE_TO(DL, "\n");
            Problems::Buffer::output_problem_buffer_to(DL, indentation);
            WRITE_TO(DL, "\n");
            if (telemetry_recording) {
                WRITE_TO(telmy, "\n");
                Problems::Buffer::output_problem_buffer_to(telmy, indentation);
                WRITE_TO(telmy, "\n");
            }
        } else Problems::Buffer::output_problem_buffer_to(probl, indentation);
    }

The function Problems::Buffer::redirect_problem_stream appears nowhere else.

The function Problems::Buffer::output_problem_buffer is used in 2/pl2 (§3.1, §9).