1
0
Fork 0
mirror of https://github.com/ganelson/inform.git synced 2024-07-03 07:24:58 +03:00
inform7/inbuild/inbuild-module/Chapter 5/Extension Services.w

348 lines
13 KiB
OpenEdge ABL
Raw Normal View History

[Extensions::] Extension Services.
2020-02-08 12:34:58 +02:00
An Inform 7 extension.
2020-02-17 02:16:38 +02:00
@
=
2020-02-08 12:34:58 +02:00
typedef struct inform_extension {
struct inbuild_copy *as_copy;
struct wording body_text; /* Body of source text supplied in extension, if any */
int body_text_unbroken; /* Does this contain text waiting to be sentence-broken? */
struct wording documentation_text; /* Documentation supplied in extension, if any */
int standard; /* the (or perhaps just a) Standard Rules extension */
2020-02-08 12:34:58 +02:00
int authorial_modesty; /* Do not credit in the compiled game */
2020-02-19 22:48:30 +02:00
struct text_stream *rubric_as_lexed; /* brief description found in opening lines */
2020-02-08 12:34:58 +02:00
struct text_stream *extra_credit_as_lexed;
2020-02-17 02:16:38 +02:00
struct source_file *read_into_file; /* Which source file loaded this */
struct inbuild_requirement *must_satisfy;
int loaded_from_built_in_area; /* Located within Inform application */
struct parse_node_tree *syntax_tree;
struct parse_node *inclusion_sentence; /* Where the source called for this */
2020-02-08 12:34:58 +02:00
MEMORY_MANAGEMENT
} inform_extension;
2020-03-29 16:48:19 +03:00
void Extensions::scan(inbuild_copy *C) {
2020-02-08 12:34:58 +02:00
inform_extension *E = CREATE(inform_extension);
2020-02-19 22:48:30 +02:00
E->as_copy = C;
Copies::set_content(C, STORE_POINTER_inform_extension(E));
2020-02-08 12:34:58 +02:00
E->body_text = EMPTY_WORDING;
E->body_text_unbroken = FALSE;
E->documentation_text = EMPTY_WORDING;
E->standard = FALSE;
2020-02-08 12:34:58 +02:00
E->authorial_modesty = FALSE;
2020-02-17 02:16:38 +02:00
E->read_into_file = NULL;
2020-02-19 22:48:30 +02:00
E->rubric_as_lexed = Str::new();
2020-02-08 12:34:58 +02:00
E->extra_credit_as_lexed = NULL;
2020-02-17 02:16:38 +02:00
E->must_satisfy = NULL;
E->loaded_from_built_in_area = FALSE;
E->syntax_tree = ParseTree::new_tree();
E->inclusion_sentence = NULL;
2020-02-19 22:48:30 +02:00
TEMPORARY_TEXT(claimed_author_name);
TEMPORARY_TEXT(claimed_title);
TEMPORARY_TEXT(reqs);
2020-02-19 22:48:30 +02:00
filename *F = C->location_if_file;
semantic_version_number V = VersionNumbers::null();
@<Scan the file@>;
if (Str::len(claimed_title) == 0) { WRITE_TO(claimed_title, "Unknown"); }
if (Str::len(claimed_author_name) == 0) { WRITE_TO(claimed_author_name, "Anonymous"); }
if (Str::len(claimed_title) > MAX_EXTENSION_TITLE_LENGTH) {
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_N(EXT_TITLE_TOO_LONG_CE, -1, Str::len(claimed_title)));
2020-02-19 22:48:30 +02:00
}
if (Str::len(claimed_author_name) > MAX_EXTENSION_AUTHOR_LENGTH) {
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_N(EXT_AUTHOR_TOO_LONG_CE, -1, Str::len(claimed_author_name)));
2020-02-19 22:48:30 +02:00
}
C->edition = Editions::new(Works::new(extension_genre, claimed_title, claimed_author_name), V);
if (Str::len(reqs) > 0) {
compatibility_specification *CS = Compatibility::from_text(reqs);
2020-02-19 22:48:30 +02:00
if (CS) C->edition->compatibility = CS;
else {
TEMPORARY_TEXT(err);
WRITE_TO(err, "cannot read compatibility '%S'", reqs);
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_T(EXT_MISWORDED_CE, -1, err));
2020-02-19 22:48:30 +02:00
DISCARD_TEXT(err);
}
}
Works::add_to_database(C->edition->work, CLAIMED_WDBC);
DISCARD_TEXT(claimed_author_name);
DISCARD_TEXT(claimed_title);
DISCARD_TEXT(reqs);
2020-02-08 12:34:58 +02:00
}
2020-02-19 22:48:30 +02:00
@ The following scans a potential extension file. If it seems malformed, a
suitable error is written to the stream |error_text|. If not, this is left
alone, and the version number is returned.
=
@<Scan the file@> =
TEMPORARY_TEXT(titling_line);
TEMPORARY_TEXT(version_text);
FILE *EXTF = Filenames::fopen_caseless(F, "r");
if (EXTF == NULL) {
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_F(OPEN_FAILED_CE, -1, F));
2020-02-19 22:48:30 +02:00
} else {
@<Read the titling line of the extension and normalise its casing@>;
@<Read the rubric text, if any is present@>;
@<Parse the version, title, author and VM requirements from the titling line@>;
fclose(EXTF);
if (Str::len(version_text) > 0) {
V = VersionNumbers::from_text(version_text);
if (VersionNumbers::is_null(V)) {
TEMPORARY_TEXT(error_text);
WRITE_TO(error_text, "the version number '%S' is malformed", version_text);
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_T(EXT_MISWORDED_CE, -1, error_text));
2020-02-19 22:48:30 +02:00
DISCARD_TEXT(error_text);
}
}
}
DISCARD_TEXT(titling_line);
DISCARD_TEXT(version_text);
@ The titling line is terminated by any of |0A|, |0D|, |0A 0D| or |0D 0A|, or
by the local |\n| for good measure.
@<Read the titling line of the extension and normalise its casing@> =
int c;
while ((c = TextFiles::utf8_fgetc(EXTF, NULL, FALSE, NULL)) != EOF) {
if (c == 0xFEFF) continue; /* skip the optional Unicode BOM pseudo-character */
if ((c == '\x0a') || (c == '\x0d') || (c == '\n')) break;
PUT_TO(titling_line, c);
}
Works::normalise_casing(titling_line);
@ In the following, all possible newlines are converted to white space, and
all white space before a quoted rubric text is ignored. We need to do this
partly because users have probably keyed a double line break before the
rubric, but also because we might have stopped reading the titling line
halfway through a line division combination like |0A 0D|, so that the first
thing we read here is a meaningless |0D|.
@<Read the rubric text, if any is present@> =
int c, found_start = FALSE;
while ((c = TextFiles::utf8_fgetc(EXTF, NULL, FALSE, NULL)) != EOF) {
if ((c == '\x0a') || (c == '\x0d') || (c == '\n') || (c == '\t')) c = ' ';
if ((c != ' ') && (found_start == FALSE)) {
if (c == '"') found_start = TRUE;
else break;
} else {
if (c == '"') break;
if (found_start) PUT_TO(E->rubric_as_lexed, c);
}
}
@ In general, once case-normalised, a titling line looks like this:
>> Version 2/070423 Of Going To The Zoo (For Glulx Only) By Cary Grant Begins Here.
and the version information, the VM restriction and the full stop are all
optional, but the division word "of" and the concluding "begin[s] here"
are not. We break it up into pieces; for speed, we won't use the lexer to
load the entire file.
@<Parse the version, title, author and VM requirements from the titling line@> =
match_results mr = Regexp::create_mr();
if (Str::get_last_char(titling_line) == '.') Str::delete_last_character(titling_line);
if ((Regexp::match(&mr, titling_line, L"(%c*) Begin Here")) ||
(Regexp::match(&mr, titling_line, L"(%c*) Begins Here"))) {
Str::copy(titling_line, mr.exp[0]);
} else {
if ((Regexp::match(&mr, titling_line, L"(%c*) Start Here")) ||
(Regexp::match(&mr, titling_line, L"(%c*) Starts Here"))) {
Str::copy(titling_line, mr.exp[0]);
}
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_T(EXT_MISWORDED_CE, -1,
2020-02-19 22:48:30 +02:00
I"the opening line does not end 'begin(s) here'"));
}
@<Scan the version text, if any, and advance to the position past Version... Of@>;
if (Regexp::match(&mr, titling_line, L"The (%c*)")) Str::copy(titling_line, mr.exp[0]);
@<Divide the remaining text into a claimed author name and title, divided by By@>;
@<Extract the VM requirements text, if any, from the claimed title@>;
Regexp::dispose_of(&mr);
@ We make no attempt to check the version number for validity: the purpose
of the census is to identify extensions and reject accidentally included
other files, not to syntax-check all extensions to see if they would work
if used.
@<Scan the version text, if any, and advance to the position past Version... Of@> =
if (Regexp::match(&mr, titling_line, L"Version (%c*?) Of (%c*)")) {
Str::copy(version_text, mr.exp[0]);
Str::copy(titling_line, mr.exp[1]);
}
@ The earliest "by" is the divider: note that extension titles are not
allowed to contain this word, so "North By Northwest By Cary Grant" is
not a situation we need to contend with.
@<Divide the remaining text into a claimed author name and title, divided by By@> =
if (Regexp::match(&mr, titling_line, L"(%c*?) By (%c*)")) {
Str::copy(claimed_title, mr.exp[0]);
Str::copy(claimed_author_name, mr.exp[1]);
} else {
Str::copy(claimed_title, titling_line);
2020-03-29 19:39:17 +03:00
Copies::attach_error(C, CopyErrors::new_T(EXT_MISWORDED_CE, -1,
2020-02-19 22:48:30 +02:00
I"the titling line does not give both author and title"));
}
@ Similarly, extension titles are not allowed to contain parentheses, so
this is unambiguous.
@<Extract the VM requirements text, if any, from the claimed title@> =
if (Regexp::match(&mr, claimed_title, L"(%c*?) *(%(%c*%))")) {
Str::copy(claimed_title, mr.exp[0]);
Str::copy(reqs, mr.exp[1]);
2020-02-19 22:48:30 +02:00
}
@ =
2020-02-17 02:16:38 +02:00
void Extensions::write(OUTPUT_STREAM, inform_extension *E) {
if (E == NULL) WRITE("none");
else WRITE("%X", E->as_copy->edition->work);
}
void Extensions::write_name_to_file(inform_extension *E, OUTPUT_STREAM) {
WRITE("%S", E->as_copy->edition->work->raw_title);
}
void Extensions::write_author_to_file(inform_extension *E, OUTPUT_STREAM) {
WRITE("%S", E->as_copy->edition->work->raw_author_name);
}
@ Three pieces of information (not available when the EF is created) will
be set later on, by other parts of Inform calling the routines below.
The rubric text for an extension, which is double-quoted matter just below
its "begins here" line, is parsed as a sentence and will be read as an
assertion in the usual way when the material from this extension is being
worked through (quite a long time after the EF structure was created). When
that happens, the following routine will be called to set the rubric; and
the one after for the optional extra credit line, used to acknowledge I6
sources, collaborators, translators and so on.
=
void Extensions::set_rubric(inform_extension *E, text_stream *text) {
if (E == NULL) internal_error("unfound ef");
E->rubric_as_lexed = Str::duplicate(text);
LOGIF(EXTENSIONS_CENSUS, "Extension rubric: %S\n", E->rubric_as_lexed);
}
2020-02-19 22:48:30 +02:00
text_stream *Extensions::get_rubric(inform_extension *E) {
if (E == NULL) return NULL;
return E->rubric_as_lexed;
}
2020-02-17 02:16:38 +02:00
void Extensions::set_extra_credit(inform_extension *E, text_stream *text) {
if (E == NULL) internal_error("unfound ef");
E->extra_credit_as_lexed = Str::duplicate(text);
LOGIF(EXTENSIONS_CENSUS, "Extension extra credit: %S\n", E->extra_credit_as_lexed);
}
@ The use option "authorial modesty" is unusual in applying to the extension
it is found in, not the whole source text. When we read it, we call one of
the following routines, depending on whether it was in an extension or in
the main source text:
=
int general_authorial_modesty = FALSE;
void Extensions::set_authorial_modesty(inform_extension *E) {
if (E == NULL) internal_error("unfound ef");
E->authorial_modesty = TRUE;
}
void Extensions::set_general_authorial_modesty(void) { general_authorial_modesty = TRUE; }
void Extensions::set_inclusion_sentence(inform_extension *E, parse_node *N) {
E->inclusion_sentence = N;
}
2020-02-17 02:16:38 +02:00
parse_node *Extensions::get_inclusion_sentence(inform_extension *E) {
if (E == NULL) return NULL;
return E->inclusion_sentence;
}
int Extensions::is_standard(inform_extension *E) {
if (E == NULL) return FALSE;
return E->standard;
}
void Extensions::make_standard(inform_extension *E) {
E->standard = TRUE;
}
2020-02-17 02:16:38 +02:00
void Extensions::must_satisfy(inform_extension *E, inbuild_requirement *req) {
if (E->must_satisfy == NULL) E->must_satisfy = req;
2020-03-23 00:45:46 +02:00
else if (VersionNumberRanges::intersect_range(E->must_satisfy->version_range, req->version_range)) {
2020-03-02 14:55:33 +02:00
#ifdef CORE_MODULE
Extensions::set_inclusion_sentence(E, current_sentence);
#endif
2020-02-17 02:16:38 +02:00
}
}
2020-02-17 02:16:38 +02:00
int Extensions::satisfies(inform_extension *E) {
if (E == NULL) return FALSE;
return Requirements::meets(E->as_copy->edition, E->must_satisfy);
}
@
=
2020-02-17 02:16:38 +02:00
void Extensions::read_source_text_for(inform_extension *E) {
filename *F = E->as_copy->location_if_file;
int doc_only = FALSE;
2020-02-23 14:36:39 +02:00
if (census_mode) doc_only = TRUE;
TEMPORARY_TEXT(synopsis);
@<Concoct a synopsis for the extension to be read@>;
2020-02-17 11:43:20 +02:00
E->read_into_file = SourceText::read_file(E->as_copy, F, synopsis, doc_only, FALSE);
DISCARD_TEXT(synopsis);
if (E->read_into_file) {
E->read_into_file->your_ref = STORE_POINTER_inbuild_copy(E->as_copy);
wording EXW = E->read_into_file->text_read;
if (Wordings::nonempty(EXW)) @<Break the extension's text into body and documentation@>;
inform_project *project = Inbuild::project();
if (project) E->syntax_tree = project->syntax_tree;
Sentences::break(E->syntax_tree, E->body_text, TRUE, E->as_copy, -1);
2020-03-03 02:59:42 +02:00
E->body_text_unbroken = FALSE;
}
}
2020-03-03 13:02:46 +02:00
inform_extension *Extensions::corresponding_to(source_file *sf) {
if (sf == NULL) return NULL;
inbuild_copy *C = RETRIEVE_POINTER_inbuild_copy(sf->your_ref);
if (C == NULL) return NULL;
if (C->edition->work->genre != extension_genre) return NULL;
return ExtensionManager::from_copy(C);
}
@ We concoct a textual synopsis in the form
|"Pantomime Sausages by Mr Punch"|
to be used by |SourceFiles::read_extension_source_text| for printing to |stdout|. Since
we dare not assume |stdout| can manage characters outside the basic ASCII
range, we flatten them from general ISO to plain ASCII.
@<Concoct a synopsis for the extension to be read@> =
WRITE_TO(synopsis, "%S by %S", E->as_copy->edition->work->title, E->as_copy->edition->work->author_name);
LOOP_THROUGH_TEXT(pos, synopsis)
Str::put(pos,
Characters::make_filename_safe(Str::get(pos)));
@ If an extension file contains the special text (outside literal mode) of
|---- Documentation ----|
then this is taken as the end of the Inform source, and the beginning of a
snippet of documentation about the extension; text from that point on is
saved until later, but not broken into sentences for the parse tree, and it
is therefore invisible to the rest of Inform. If this division line is not
present then the extension contains only body source and no documentation.
=
<extension-body> ::=
*** ---- documentation ---- ... | ==> TRUE
... ==> FALSE
@<Break the extension's text into body and documentation@> =
<extension-body>(EXW);
E->body_text = GET_RW(<extension-body>, 1);
if (<<r>>) E->documentation_text = GET_RW(<extension-body>, 2);
E->body_text_unbroken = TRUE; /* mark this to be sentence-broken */