Cascading style sheets for HTML output.


§1. The whole idea of a CSS file is to provide styling independent from content, and since indoc generates content, it shouldn't meddle with CSS files at all. It does so for two reasons:

typedef struct CSS_tweak_data {
    struct text_stream *css_style;  the CSS style name to tweak
    struct text_stream *css_tweak;  the instruction
    int css_tweaked;  has this instruction happened yet?
    int css_plus;  1 for "augment this style", 0 for "replace this style"
    int css_to_be_added;
    struct volume *within_volume;
    CLASS_DEFINITION
} CSS_tweak_data;

§2. Span notations allow markup such as this is *dreadful* to represent emphasis; and are also used to mark headwords for indexing, as in this is ^{nifty}.

define MAX_PATTERN_LENGTH 1024
define MARKUP_SPP 1
define INDEX_TEXT_SPP 2
define INDEX_SYMBOLS_SPP 3
typedef struct span_notation {
    int sp_purpose;                          one of the *_SPP constants
    inchar32_t sp_left[MAX_PATTERN_LENGTH];      wide C string: the start pattern
    int sp_left_len;
    inchar32_t sp_right[MAX_PATTERN_LENGTH];     wide C string: and end pattern
    int sp_right_len;
    struct text_stream *sp_style;
    CLASS_DEFINITION
} span_notation;

§3. Requesting CSS tweaks. The null volume means "in all volumes".

void CSS::request_css_tweak(volume *V, text_stream *style, text_stream *want, int plus) {
    if (V == NULL) {
        LOOP_OVER(V, volume)
            CSS::request_css_tweak(V, style, want, plus);
    } else {
        CSS_tweak_data *TD = CREATE(CSS_tweak_data);
        TD->css_style = Str::duplicate(style);
        TD->css_tweak = Str::duplicate(want);
        TD->css_tweaked = FALSE;
        TD->css_plus = 1;
        TD->css_to_be_added = FALSE;
        TD->within_volume = V;
        if (plus == 2) TD->css_to_be_added = TRUE;
    }
}

§4. Constructing CSS files. There's one CSS file for each volume.

void CSS::write_CSS_files(filename *from) {
    volume *V;
    LOOP_OVER(V, volume) {
        TEMPORARY_TEXT(leafname)
        WRITE_TO(leafname, "indoc");
        if (no_volumes > 1) WRITE_TO(leafname, "_%S", V->vol_abbrev);
        WRITE_TO(leafname, ".css");
        V->vol_CSS_leafname = Str::duplicate(leafname);
        filename *F = Filenames::in(indoc_settings->destination, leafname);

        text_stream CSS_struct;
        text_stream *OUT = &CSS_struct;
        if (Streams::open_to_file(OUT, F, UTF8_ENC) == FALSE)
            Errors::fatal_with_file("can't write CSS file", F);
        if (indoc_settings->verbose_mode) PRINT("[Writing %/f]\n", F);

        Construct the CSS for this volume4.1;
        Add any missing CSS material to this volume4.2;
        Streams::close(OUT);
    }
    Check that all of the tweaks have had an effect4.3;
}

§4.1. Construct the CSS for this volume4.1 =

    CSS_helper_state chs;
    chs.tx_mode = FALSE;
    chs.this_style = Str::new();
    chs.this_style_tweaked = FALSE;
    chs.V = V;
    chs.OUT = OUT;
    TextFiles::read(from, FALSE, "can't open CSS file model",
        TRUE, CSS::construct_helper, NULL, &chs);

§5.

typedef struct CSS_helper_state {
    int tx_mode;
    struct text_stream *this_style;
    int this_style_tweaked;
    struct volume *V;
    struct text_stream *OUT;
} CSS_helper_state;

void CSS::construct_helper(text_stream *line, text_file_position *tfp,
    void *v_chs) {
    CSS_helper_state *chs = (CSS_helper_state *) v_chs;
    text_stream *OUT = chs->OUT;
    Str::trim_white_space_at_end(line);
    if (chs->tx_mode) {
        Apply CSS modifications requested by the instructions5.1;
        CSS::expand_IMAGES(line);
        WRITE("%S\n", line);
    } else {
        if (Regexp::match(NULL, line, U"%c*BEGIN%c*")) chs->tx_mode = TRUE;
    }
}

§5.1. We detect the style opening and closing in a way which wouldn't work for all CSS files, but here we know that base.css is tidily written.

Apply CSS modifications requested by the instructions5.1 =

    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, line, U"(%c*?) *{"))
        Str::copy(chs->this_style, mr.exp[0]);
    else if (Regexp::match(&mr, line, U" *}")) {
        Str::clear(chs->this_style); chs->this_style_tweaked = 0;
    } else {
        CSS_tweak_data *TD;
        LOOP_OVER(TD, CSS_tweak_data) {
            if ((Str::eq(TD->css_style, chs->this_style)) && (TD->within_volume == chs->V)) {
                if (chs->this_style_tweaked == FALSE) Add the new CSS lines supplied5.1.1;
                Decide whether to keep the original CSS line5.1.2;
            }
        }
    }
    Regexp::dispose_of(&mr);

§5.1.1. Add the new CSS lines supplied5.1.1 =

    TEMPORARY_TEXT(want)
    Str::copy(want, TD->css_tweak);
    CSS::expand_IMAGES(want);
    WRITE("%S", want);
    DISCARD_TEXT(want)
    chs->this_style_tweaked = TRUE;
    TD->css_tweaked = TRUE;
    if (TD->css_plus > 0)
        CSS::alert_CSS_change(I"Augmenting", chs->this_style, chs->V);
    else
        CSS::alert_CSS_change(I"Replacing", chs->this_style, chs->V);

§5.1.2. Decide whether to keep the original CSS line5.1.2 =

    int keep = FALSE;
    if (TD->css_plus > 0) {
        keep = TRUE;
        match_results mr2 = Regexp::create_mr();
        if (Regexp::match(&mr2, line, U" *(%C+):%c*")) {
            text_stream *tag = mr2.exp[0];
            if (Str::includes(TD->css_tweak, tag)) keep = FALSE;
        }
        Regexp::dispose_of(&mr2);
    }
    if (keep == FALSE) { Regexp::dispose_of(&mr); return; }

§4.2. Add any missing CSS material to this volume4.2 =

    CSS_tweak_data *TD;
    LOOP_OVER(TD, CSS_tweak_data) {
        if ((TD->within_volume == V) && (TD->css_tweaked == FALSE) && (TD->css_to_be_added)) {
            text_stream *tag = TD->css_style;
            CSS::alert_CSS_change(I"Adding", tag, V);
            WRITE("%S {\n", tag);
            TEMPORARY_TEXT(expanded)
            Str::copy(expanded, TD->css_tweak);
            CSS::expand_IMAGES(expanded);
            WRITE("%S}\n", expanded);
            TD->css_tweaked = TRUE;
        }
    }

§4.3. Check that all of the tweaks have had an effect4.3 =

    CSS_tweak_data *TD;
    LOOP_OVER(TD, CSS_tweak_data) {
        if ((TD->within_volume == V) && (TD->css_tweaked == FALSE)) {
            text_stream *tag = TD->css_style;
            TEMPORARY_TEXT(err)
            WRITE_TO(err, "CSS modification had no effect: failed to ");
            if (TD->css_plus > 0) WRITE_TO(err, "augment");
            else WRITE_TO(err, "replace");
            WRITE_TO(err, " the style \"%S\"", tag);
            Errors::in_text_file_S(err, NULL);
            DISCARD_TEXT(err)
        }
    }

§6.

void CSS::expand_IMAGES(text_stream *text) {
    match_results mr = Regexp::create_mr();
    if (indoc_settings->suppress_fonts) {
        while (Regexp::match(&mr, text, U"(%c*?)font-family:(%c*?);(%c*)")) {
            text_stream *L = mr.exp[0], *M = NULL, *R = mr.exp[2];
            if (Regexp::match(NULL, mr.exp[1], U"%c*monospace%c*")) M = I"___MONOSPACE___";
            Str::clear(text);
            WRITE_TO(text, "%S%S%S", L, M, R);
        }
        Regexp::replace(text, U"___MONOSPACE___", U"font-family: monospace;", REP_REPEATING);
    }
    while (Regexp::match(&mr, text, U"(%c*?)'IMAGES/(%c*?)'(%c*)")) {
        text_stream *L = mr.exp[0], *name = mr.exp[1], *R = mr.exp[2];
        Str::clear(text);
        WRITE_TO(text, "%S'", L);
        HTMLUtilities::image_URL(text, name);
        WRITE_TO(text, "'%S", R);
    }
    Regexp::dispose_of(&mr);
}

§7. Verbosity.

void CSS::alert_CSS_change(text_stream *what, text_stream *this_style, volume *V) {
    if (indoc_settings->verbose_mode)
        PRINT("%S CSS style \"%S\" in volume \"%S\"\n", what, this_style, V->vol_title);
}

§8. Span notations.

void CSS::add_span_notation(text_stream *L, text_stream *R, text_stream *style, int purpose) {
    span_notation *SN = CREATE(span_notation);
    SN->sp_style = Str::duplicate(style);
    Str::copy_to_wide_string(SN->sp_left, L, MAX_PATTERN_LENGTH);
    Str::copy_to_wide_string(SN->sp_right, R, MAX_PATTERN_LENGTH);
    SN->sp_left_len = Str::len(L);
    SN->sp_right_len = Str::len(R);
    SN->sp_purpose = purpose;
}

§9. We have to escape any characters which might be special to our regular expression parser:

void CSS::make_regex_safe(text_stream *text) {
    Regexp::replace(text, U"%%", U"%%%", REP_REPEATING);
    Regexp::replace(text, U"%(", U"%%(", REP_REPEATING);
    Regexp::replace(text, U"%)", U"%%)", REP_REPEATING);
    Regexp::replace(text, U"%+", U"%%+", REP_REPEATING);
    Regexp::replace(text, U"%*", U"%%*", REP_REPEATING);
    Regexp::replace(text, U"%?", U"%%?", REP_REPEATING);
    Regexp::replace(text, U"%[", U"%%[", REP_REPEATING);
    Regexp::replace(text, U"%]", U"%%]", REP_REPEATING);
}

§10. The following looks slow, but in fact there's no problem in practice.

void CSS::expand_spannotations(text_stream *text, int purpose) {
    TEMPORARY_TEXT(modified)
    int changes = 0;
    for (int i=0, L = Str::len(text); i<L; i++) {
        int claimed = FALSE;
        span_notation *SN;
        LOOP_OVER(SN, span_notation)
            if (SN->sp_purpose == purpose) {
                if (Str::includes_wide_string_at(text, SN->sp_left, i)) {
                    if (changes++ == 0)
                        for (int j=0; j<i; j++) PUT_TO(modified, Str::get_at(text, j));
                    int content_from = i + SN->sp_left_len, content_to = -1;
                    for (int k2 = content_from; k2<L; k2++)
                        if (Str::includes_wide_string_at(text, SN->sp_right, k2)) {
                             content_to = k2; break;
                        }
                    if (content_to >= 0) {
                        WRITE_TO(modified, "___mu___%S___mo___", SN->sp_style);
                        for (int j=content_from; j<content_to; j++)
                            PUT_TO(modified, Str::get_at(text, j));
                        WRITE_TO(modified, "___mc___");
                        i = content_to + SN->sp_right_len - 1;
                    } else {
                        PUT_TO(modified, Str::get_at(text, i));
                    }
                    claimed = TRUE;
                }
            }
        if ((changes > 0) && (claimed == FALSE))
            PUT_TO(modified, Str::get_at(text, i));
    }
    if (changes > 0) Str::copy(text, modified);
    DISCARD_TEXT(modified)
}