Provides conditional rendering of text, depending on context.


§1. About symbols. Documentation is allowed to contain rawtext which varies depending on the context: on what platform it's being written for, or what format it is being rendered to, for example. The context is defined by which symbol names have been defined.

Symbols are C-like identifiers (alphanumeric or underscored). The symbol has been declared if and only if it is an existing key for the following hash, and the associated value is "y".

dictionary *defined_symbols = NULL;

§2. Starting up. The symbol indoc is always defined, so that, in theory, other programs working on rawtext can distinguish themselves from us (by not defining it). For example,

    {^indoc:}You'll probably never see this paragraph.

provides rawtext visible only if indoc isn't the renderer.

void Symbols::start_up_symbols(void) {
    Symbols::declare_symbol(I"indoc");
}

§3. Making and unmaking.

void Symbols::declare_symbol(text_stream *symbol) {
    if (defined_symbols == NULL) defined_symbols = Dictionaries::new(10, TRUE);
    text_stream *entry = Dictionaries::create_text(defined_symbols, symbol);
    Str::copy(entry, I"y");
    LOGIF(SYMBOLS, "Declaring <%S>\n", symbol);
}

void Symbols::undeclare_symbol(text_stream *symbol) {
    text_stream *entry = Dictionaries::get_text(defined_symbols, symbol);
    if (entry == NULL) return;
    Str::copy(entry, I"n");
    LOGIF(SYMBOLS, "Undeclaring <%S>\n", symbol);
}

§4. Testing. This returns 1 if the current context matches the condition given, and 0 otherwise.

int Symbols::perform_ifdef(text_stream *cond) {
    for (int i=0, L=Str::len(cond); i<L; i++) {
        inchar32_t c = Str::get_at(cond, i);
        if (Characters::is_whitespace(c)) {
            Str::delete_nth_character(cond, i);
            i--; L--;
        }
    }
    int v = Symbols::perform_ifdef_inner(cond);
    LOGIF(SYMBOLS, "Ifdef <%S> --> %s\n", cond, v?"yes":"no");
    return v;
}

§5. There is an expression grammar here, which we apply correctly if the condition is well-formed; if it's a mess, we try to return 0, but don't go to any trouble to report errors.

Any condition can be bracketed; otherwise we have the unary operator ^ (negation), the binary + (conjunction), and binary , (disjunction), which associate in that order. An atomic condition is true if and only if it is a declared symbol. So for example

^alpha,beta+gamma

is true if either alpha is undeclared, or if both beta and gamma are declared. Whereas

^(alpha,beta)+gamma ^alpha+^beta+gamma

are both true if alpha and beta are undeclared but gamma is declared.

int Symbols::perform_ifdef_inner(text_stream *cond) {
    Subexpressions can be bracketed5.1;
    The comma operator is left-associative and means or5.2;
    The plus operator is left-associative and means and5.3;
    The caret operator is unary and means not5.4;
    A bare symbol name is true if and only if it is declared5.5;

    The expression is malformed5.6;
}

§5.1. Subexpressions can be bracketed5.1 =

    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, cond, U"%((%c*)%)")) {
        int rv = Symbols::perform_ifdef(mr.exp[0]);
        Regexp::dispose_of(&mr);
        return rv;
    }

§5.2. The comma operator is left-associative and means or5.2 =

    int k = Symbols::find_operator(cond, ',');
    if (k >= 0) {
        TEMPORARY_TEXT(L)
        TEMPORARY_TEXT(R)
        Str::copy(L, cond);
        Str::truncate(L, k);
        Str::copy_tail(R, cond, k+1);
        int rv = ((Symbols::perform_ifdef(L)) || (Symbols::perform_ifdef(R)));
        DISCARD_TEXT(L)
        DISCARD_TEXT(R)
        return rv;
    } else if (k == -2) The expression is malformed5.6;

§5.3. The plus operator is left-associative and means and5.3 =

    int k = Symbols::find_operator(cond, '+');
    if (k >= 0) {
        TEMPORARY_TEXT(L)
        TEMPORARY_TEXT(R)
        Str::copy(L, cond);
        Str::truncate(L, k);
        Str::copy_tail(R, cond, k+1);
        int rv = ((Symbols::perform_ifdef(L)) && (Symbols::perform_ifdef(R)));
        DISCARD_TEXT(L)
        DISCARD_TEXT(R)
        return rv;
    } else if (k == -2) The expression is malformed5.6;

§5.4. The caret operator is unary and means not5.4 =

    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, cond, U"%^(%c*)")) {
        int rv = Symbols::perform_ifdef(mr.exp[0]);
        Regexp::dispose_of(&mr);
        return rv?FALSE:TRUE;
    }

§5.5. A bare symbol name is true if and only if it is declared5.5 =

    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, cond, U"%i+")) {
        Regexp::dispose_of(&mr);
        text_stream *entry = Dictionaries::get_text(defined_symbols, cond);
        if (Str::eq_wide_string(entry, U"y")) return TRUE;
        return FALSE;
    }

§5.6. The expression is malformed5.6 =

    Errors::with_text("malformed condition: %S", cond); return 0;

§6. The following looks for a single character op in any unbracketed interior parts of the text cond, and looks out for mismatched brackets on the way.

int Symbols::find_operator(text_stream *cond, inchar32_t op) {
    int bl = 0;
    for (int k = 0, L = Str::len(cond); k < L; k++) {
        inchar32_t ch = Str::get_at(cond, k);
        if (ch == '(') bl++;
        if (ch == ')') bl--;
        if (bl < 0) return -2;  Too many close brackets
        if ((bl == 0) && (k > 0) && (k < L - 1) && (ch == op)) {
            return k;
        }
    }
    if (bl != 0) return -2;  Too many open brackets
    return -1;  Not found
}