To construct Readme and similar files.

§1. This is a very simple generator for README.md files, written in Markdown syntax, but with a few macro expansions of our own. The prototype file, which uses these extra macros, is expanded to the final file, which does not.

As we scan through the prototype file, we keep track of this:

typedef struct write_state {
    struct text_stream *OUT;
    struct linked_list *known_macros;  of macro
    struct macro *current_definition;
    struct macro_tokens *stack_frame;
} write_state;

void Readme::write(filename *from, filename *to) {
    WRITE_TO(STDOUT, "write-me: %f --> %f\n", from, to);
    write_state ws;
    ws.current_definition = NULL;
    ws.known_macros = NEW_LINKED_LIST(macro);
    macro *V = Readme::new_macro(I"version", NULL, NULL);
    ADD_TO_LINKED_LIST(V, macro, ws.known_macros);
    macro *P = Readme::new_macro(I"purpose", NULL, NULL);
    ADD_TO_LINKED_LIST(P, macro, ws.known_macros);
    macro *A = Readme::new_macro(I"var", NULL, NULL);
    ADD_TO_LINKED_LIST(A, macro, ws.known_macros);
    ws.stack_frame = NULL;
    text_stream file_to;
    if (Streams::open_to_file(&file_to, to, UTF8_ENC) == FALSE)
        Errors::fatal_with_file("can't write readme file", to);
    ws.OUT = &file_to;
    TextFiles::read(from, FALSE, "unable to read template file", TRUE,
        &Readme::write_helper, NULL, (void *) &ws);
    Streams::close(&file_to);
}

§2. The file consists of definitions of macros, made one at a time, and starting with @define and finishing with @end, and actual material.

void Readme::write_helper(text_stream *text, text_file_position *tfp, void *state) {
    write_state *ws = (write_state *) state;
    text_stream *OUT = ws->OUT;

    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, text, L" *@end *")) {
        if (ws->current_definition == NULL)
            Errors::in_text_file("@end without @define", tfp);
        else ws->current_definition = NULL;
    } else if (ws->current_definition) {
        if (Str::len(ws->current_definition->content) > 0)
            WRITE_TO(ws->current_definition->content, "\n");
        WRITE_TO(ws->current_definition->content, "%S", text);
    } else if (Regexp::match(&mr, text, L" *@define (%i+)(%c*)")) {
        if (ws->current_definition)
            Errors::in_text_file("@define without @end", tfp);
        else {
            macro *M = Readme::new_macro(mr.exp[0], mr.exp[1], tfp);
            ws->current_definition = M;
            ADD_TO_LINKED_LIST(M, macro, ws->known_macros);
        }
    } else {
        Readme::expand_material(ws, OUT, text, tfp);
        Readme::expand_material(ws, OUT, I"\n", tfp);
    }
    Regexp::dispose_of(&mr);
}

§3. The "content" of a macro is its definition, and the tokens are named parameters.

typedef struct macro {
    struct text_stream *name;
    struct text_stream *content;
    struct macro_tokens tokens;
    CLASS_DEFINITION
} macro;

macro *Readme::new_macro(text_stream *name, text_stream *tokens, text_file_position *tfp) {
    macro *M = CREATE(macro);
    M->name = Str::duplicate(name);
    M->tokens = Readme::parse_token_list(tokens, tfp);
    M->content = Str::new();
    return M;
}

typedef struct macro_tokens {
    struct macro *bound_to;
    struct text_stream *pars[8];
    int no_pars;
    struct macro_tokens *down;
    CLASS_DEFINITION
} macro_tokens;

§4.

macro_tokens Readme::parse_token_list(text_stream *chunk, text_file_position *tfp) {
    macro_tokens mt;
    mt.no_pars = 0;
    mt.down = NULL;
    mt.bound_to = NULL;
    if (Str::get_first_char(chunk) == '(') {
        int x = 1, bl = 1, from = 1, quoted = FALSE;
        while ((bl > 0) && (Str::get_at(chunk, x) != 0)) {
            wchar_t c = Str::get_at(chunk, x);
            if (c == '\'') {
                quoted = quoted?FALSE:TRUE;
            } else if (quoted == FALSE) {
                if (c == '(') bl++;
                else if (c == ')') {
                    bl--;
                    if (bl == 0) Recognise token4.1;
                } else if ((c == ',') && (bl == 1)) Recognise token4.1;
            }
            x++;
        }
        Str::delete_n_characters(chunk, x);
    }
    return mt;
}

§4.1. Quotes can be used in token lists so that literal commas and brackets can be used without breaking the flow.

Recognise token4.1 =

    int n = mt.no_pars;
    if (n >= 8) Errors::in_text_file("too many parameters", tfp);
    else {
        mt.pars[n] = Str::new();
        for (int j=from; j<x; j++) PUT_TO(mt.pars[n], Str::get_at(chunk, j));
        Str::trim_white_space(mt.pars[n]);
        if ((Str::get_first_char(mt.pars[n]) == '\'') &&
            (Str::get_last_char(mt.pars[n]) == '\'')) {
            Str::delete_first_character(mt.pars[n]);
            Str::delete_last_character(mt.pars[n]);
        }
        mt.no_pars++;
    }
    from = x+1;

§5. So much for creating macros. Now we can write the actual expander. As can be seen, it passes material straight through, except for instances of the notation @name, possibly followed by a bracketed list of parameters.

void Readme::expand_material(write_state *ws, text_stream *OUT, text_stream *text,
    text_file_position *tfp) {
    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, text, L"(%c*?)@(%i+)(%c*)")) {
        Readme::expand_material(ws, OUT, mr.exp[0], tfp);
        macro_tokens mt = Readme::parse_token_list(mr.exp[2], tfp);
        mt.down = ws->stack_frame;
        ws->stack_frame = &mt;
        Readme::expand_at(ws, OUT, mr.exp[1], tfp);
        ws->stack_frame = mt.down;
        Readme::expand_material(ws, OUT, mr.exp[2], tfp);
    } else {
        WRITE("%S", text);
    }
    Regexp::dispose_of(&mr);
}

§6. If we run into the notation @something, it's possible that something is the name of a parameter somewhere in the current stack, either on the top frame or on frames lower down. The first match wins... and if there are no matches, then it must be a macro name.

void Readme::expand_at(write_state *ws, text_stream *OUT, text_stream *macro_name,
    text_file_position *tfp) {
    macro_tokens *stack = ws->stack_frame;
    while (stack) {
        macro *in = stack->bound_to;
        if (in)
            for (int n = 0; n < in->tokens.no_pars; n++)
                if (Str::eq(in->tokens.pars[n], macro_name)) {
                    if (n < stack->no_pars) {
                        Readme::expand_material(ws, OUT, stack->pars[n], tfp);
                        return;
                    }
                }
        stack = stack->down;
    }

    macro *M;
    LOOP_OVER_LINKED_LIST(M, macro, ws->known_macros)
        if (Str::eq(M->name, macro_name)) {
            ws->stack_frame->bound_to = M;
            Readme::expand_macro(ws, OUT, M, tfp);
            return;
        }
    Errors::in_text_file("no such @-command", tfp);
    WRITE_TO(STDERR, "(command is '%S')\n", macro_name);
}

§7. So, then: suppose we have to expand @example(5, gold rings). Then the macro_name below is set to example, and the current stack frame contains the values 5 and gold rings.

void Readme::expand_macro(write_state *ws, text_stream *OUT, macro *M, text_file_position *tfp) {
    if (Str::eq(M->name, I"version")) Perform built-in expansion of version macro7.1
    else if (Str::eq(M->name, I"purpose")) Perform built-in expansion of purpose macro7.2
    else if (Str::eq(M->name, I"var")) Perform built-in expansion of var macro7.3
    else {
        ws->stack_frame->bound_to = M;
        Readme::expand_material(ws, OUT, M->content, tfp);
    }
}

§7.1. Perform built-in expansion of version macro7.1 =

    if (ws->stack_frame->no_pars != 1)
        Errors::in_text_file("@version takes 1 parameter", tfp);
    else {
        TEMPORARY_TEXT(program)
        Readme::expand_material(ws, program, ws->stack_frame->pars[0], tfp);
        Readme::write_var(OUT, program, I"Version Number");
        DISCARD_TEXT(program)
    }

§7.2. Perform built-in expansion of purpose macro7.2 =

    if (ws->stack_frame->no_pars != 1)
        Errors::in_text_file("@purpose takes 1 parameter", tfp);
    else {
        TEMPORARY_TEXT(program)
        Readme::expand_material(ws, program, ws->stack_frame->pars[0], tfp);
        Readme::write_var(OUT, program, I"Purpose");
        DISCARD_TEXT(program)
    }

§7.3. Perform built-in expansion of var macro7.3 =

    if (ws->stack_frame->no_pars != 2)
        Errors::in_text_file("@var takes 2 parameters", tfp);
    else {
        TEMPORARY_TEXT(program)
        TEMPORARY_TEXT(bibv)
        Readme::expand_material(ws, program, ws->stack_frame->pars[0], tfp);
        Readme::expand_material(ws, bibv, ws->stack_frame->pars[1], tfp);
        Readme::write_var(OUT, program, bibv);
        DISCARD_TEXT(program)
        DISCARD_TEXT(bibv)
    }

§8. An "asset" here is something for which we might want to write the version number of, or some similar metadata for. Assets are usually webs, but can also be a few other rather Inform-specific things; those have a more limited range of bibliographic data, just the version and date (and we will not assume that the version complies with any format).

typedef struct writeme_asset {
    struct text_stream *name;
    struct web_md *if_web;
    struct text_stream *date;
    struct text_stream *version;
    int next_is_version;
    CLASS_DEFINITION
} writeme_asset;

void Readme::write_var(text_stream *OUT, text_stream *program, text_stream *datum) {
    writeme_asset *A = Readme::find_asset(program);
    if (A->if_web) WRITE("%S", Bibliographic::get_datum(A->if_web, datum));
    else if (Str::eq(datum, I"Build Date")) WRITE("%S", A->date);
    else if (Str::eq(datum, I"Version Number")) WRITE("%S", A->version);
}

§9. That just leaves the business of inspecting assets to obtain their metadata.

writeme_asset *Readme::find_asset(text_stream *program) {
    writeme_asset *A;
    LOOP_OVER(A, writeme_asset) if (Str::eq(program, A->name)) return A;
    A = CREATE(writeme_asset);
    A->name = Str::duplicate(program);
    A->if_web = NULL;
    A->date = Str::new();
    A->version = Str::new();
    A->next_is_version = FALSE;
    Read in the asset9.1;
    return A;
}

§9.1. Read in the asset9.1 =

    if (Str::ends_with_wide_string(program, L".i7x")) {
        Read in the extension file9.1.1;
    } else {
        if (WebMetadata::directory_looks_like_a_web(Pathnames::from_text(program))) {
            A->if_web = WebMetadata::get_without_modules(Pathnames::from_text(program), NULL);
        } else {
            filename *I6_vn = Filenames::in(
                Pathnames::down(Pathnames::from_text(program), I"inform6"), I"header.h");
            if (TextFiles::exists(I6_vn)) Read in I6 source header file9.1.2;
            filename *template_vn = Filenames::in(Pathnames::from_text(program), I"(manifest).txt");
            if (TextFiles::exists(template_vn)) Read in template manifest file9.1.3;
            filename *rmt_vn = Filenames::in(Pathnames::from_text(program), I"README.txt");
            if (TextFiles::exists(rmt_vn)) Read in README file9.1.4;
            rmt_vn = Filenames::in(Pathnames::from_text(program), I"README.md");
            if (TextFiles::exists(rmt_vn)) Read in README file9.1.4;
        }
    }

§9.1.1. Read in the extension file9.1.1 =

    TextFiles::read(Filenames::from_text(program), FALSE, "unable to read extension", TRUE,
        &Readme::extension_harvester, NULL, A);

§9.1.2. Read in I6 source header file9.1.2 =

    TextFiles::read(I6_vn, FALSE, "unable to read header file from I6 source", TRUE,
        &Readme::header_harvester, NULL, A);

§9.1.3. Read in template manifest file9.1.3 =

    TextFiles::read(template_vn, FALSE, "unable to read manifest file from website template", TRUE,
        &Readme::template_harvester, NULL, A);

§9.1.4. Read in README file9.1.4 =

    TextFiles::read(rmt_vn, FALSE, "unable to read README file from website template", TRUE,
        &Readme::readme_harvester, NULL, A);

§10. The format for the contents section of a web is documented in Inweb.

void Readme::extension_harvester(text_stream *text, text_file_position *tfp, void *state) {
    writeme_asset *A = (writeme_asset *) state;
    match_results mr = Regexp::create_mr();
    if (Str::len(text) == 0) return;
    if (Regexp::match(&mr, text, L" *Version (%c*?) of %c*begins here. *"))
        A->version = Str::duplicate(mr.exp[0]);
    Regexp::dispose_of(&mr);
}

§11. Explicit code to read from header.h in the Inform 6 repository.

void Readme::header_harvester(text_stream *text, text_file_position *tfp, void *state) {
    writeme_asset *A = (writeme_asset *) state;
    match_results mr = Regexp::create_mr();
    if (Str::len(text) == 0) return;
    if (Regexp::match(&mr, text, L"#define RELEASE_NUMBER (%c*?) *"))
        A->version = Str::duplicate(mr.exp[0]);
    if (Regexp::match(&mr, text, L"#define RELEASE_DATE \"(%c*?)\" *"))
        A->date = Str::duplicate(mr.exp[0]);
    Regexp::dispose_of(&mr);
}

§12. Explicit code to read from the manifest file of a website template.

void Readme::template_harvester(text_stream *text, text_file_position *tfp, void *state) {
    writeme_asset *A = (writeme_asset *) state;
    match_results mr = Regexp::create_mr();
    if (Str::len(text) == 0) return;
    if (Regexp::match(&mr, text, L"%[INTERPRETERVERSION%]")) {
        A->next_is_version = TRUE;
    } else if (A->next_is_version) {
        A->version = Str::duplicate(text);
        A->next_is_version = FALSE;
    }
    Regexp::dispose_of(&mr);
}

§13. And this is needed for cheapglk and glulxe in the Inform repository.

void Readme::readme_harvester(text_stream *text, text_file_position *tfp, void *state) {
    writeme_asset *A = (writeme_asset *) state;
    match_results mr = Regexp::create_mr();
    if (Str::len(text) == 0) return;
    if ((Regexp::match(&mr, text, L"CheapGlk Library: version (%c*?) *")) ||
        (Regexp::match(&mr, text, L"- Version (%c*?) *")))
        A->version = Str::duplicate(mr.exp[0]);
    Regexp::dispose_of(&mr);
}