1
0
Fork 0
mirror of https://github.com/ganelson/inform.git synced 2024-07-08 10:04:21 +03:00
inform7/inblorb/Chapter 1/Blurb Parser.w
2019-02-09 23:58:02 +00:00

389 lines
17 KiB
OpenEdge ABL
Executable file

[Parser::] Blurb Parser.
To read and follow the instructions in the blurb file, our main input.
@h Reading the file.
We divide the file into blurb commands at line breaks, so:
=
void Parser::parse_blurb_file(filename *F) {
TextFiles::read(F, FALSE, "can't open blurb file", TRUE, Parser::interpret_line, 0, NULL);
BlorbErrors::set_error_position(NULL);
}
@ The sequence of values enumerated here must correspond exactly to
indexes into the syntaxes table below.
@d author_COMMAND 0
@d auxiliary_COMMAND 1
@d base64_COMMAND 2
@d copyright_COMMAND 3
@d cover_COMMAND 4
@d css_COMMAND 5
@d ifiction_COMMAND 6
@d ifiction_public_COMMAND 7
@d ifiction_file_COMMAND 8
@d interpreter_COMMAND 9
@d palette_COMMAND 10
@d palette_16_bit_COMMAND 11
@d palette_32_bit_COMMAND 12
@d picture_scaled_COMMAND 13
@d picture_COMMAND 14
@d picture_text_COMMAND 15
@d picture_noid_COMMAND 16
@d picture_with_alt_text_COMMAND 17
@d placeholder_COMMAND 18
@d project_folder_COMMAND 19
@d release_COMMAND 20
@d release_file_COMMAND 21
@d release_file_from_COMMAND 22
@d release_source_COMMAND 23
@d release_to_COMMAND 24
@d resolution_max_COMMAND 25
@d resolution_min_max_COMMAND 26
@d resolution_min_COMMAND 27
@d resolution_COMMAND 28
@d solution_COMMAND 29
@d solution_public_COMMAND 30
@d sound_music_COMMAND 31
@d sound_repeat_COMMAND 32
@d sound_forever_COMMAND 33
@d sound_song_COMMAND 34
@d sound_COMMAND 35
@d sound_text_COMMAND 36
@d sound_noid_COMMAND 37
@d sound_with_alt_text_COMMAND 38
@d source_COMMAND 39
@d source_public_COMMAND 40
@d status_COMMAND 41
@d status_alternative_COMMAND 42
@d status_instruction_COMMAND 43
@d storyfile_include_COMMAND 44
@d storyfile_COMMAND 45
@d storyfile_leafname_COMMAND 46
@d template_path_COMMAND 47
@d website_COMMAND 48
@ A single number specifying various possible combinations of operands:
@d OPS_NO 1
@d OPS_1TEXT 2
@d OPS_2TEXT 3
@d OPS_2TEXT_1NUMBER 4
@d OPS_1NUMBER 5
@d OPS_2NUMBER 6
@d OPS_1NUMBER_1TEXT 7
@d OPS_1NUMBER_2TEXTS 8
@d OPS_1NUMBER_1TEXT_1NUMBER 9
@d OPS_3NUMBER 10
@d OPS_3TEXT 11
@ Each legal command syntax is stored as one of these structures.
=
typedef struct blurb_command {
char *explicated; /* plain English form of the command */
wchar_t *prototype; /* regular expression prototype */
int operands; /* one of the above |OPS_*| codes */
int deprecated;
} blurb_command;
@ And here they all are. They are tested in the sequence given, and
the sequence must exactly match the numbering of the |*_COMMAND|
values above, since those are indexes into this table.
In blurb syntax, a line whose first non-white-space character is an
exclamation mark |!| is a comment, and is ignored. (This is the I6
comment character, too.) It appears in the table as a command
but, as we shall see, has no effect.
=
blurb_command syntaxes[] = {
{ "author \"name\"", L"author \"(%q*)\"", OPS_1TEXT, FALSE },
{ "auxiliary \"filename\" \"description\" \"subfolder\"",
L"auxiliary \"(%q*)\" \"(%q*)\" \"(%q*)\"", OPS_3TEXT, FALSE },
{ "base64 \"filename\" to \"filename\"",
L"base64 \"(%q*)\" to \"(%q*)\"", OPS_2TEXT, FALSE },
{ "copyright \"message\"", L"copyright \"(%q*)\"", OPS_1TEXT, FALSE },
{ "cover \"filename\"", L"cover \"(%q*)\"", OPS_1TEXT, FALSE },
{ "css", L"css", OPS_NO, FALSE },
{ "ifiction", L"ifiction", OPS_NO, FALSE },
{ "ifiction public", L"ifiction public", OPS_NO, FALSE },
{ "ifiction \"filename\" include", L"ifiction \"(%q*)\" include", OPS_1TEXT, FALSE },
{ "interpreter \"interpreter-name\" \"vm-letter\"",
L"interpreter \"(%q*)\" \"([gz])\"", OPS_2TEXT, FALSE },
{ "palette { details }", L"palette {(%c*?)}", OPS_1TEXT, TRUE },
{ "palette 16 bit", L"palette 16 bit", OPS_NO, TRUE },
{ "palette 32 bit", L"palette 32 bit", OPS_NO, TRUE },
{ "picture ID \"filename\" scale ...",
L"picture (%i+?) \"(%q*)\" scale (%c*)", OPS_3TEXT, TRUE },
{ "picture N \"filename\"", L"picture (%d+) \"(%q*)\"", OPS_1NUMBER_1TEXT, FALSE },
{ "picture ID \"filename\"", L"picture (%i+) \"(%q*)\"", OPS_2TEXT, FALSE },
{ "picture \"filename\"", L"picture \"(%q*)\"", OPS_1TEXT, FALSE },
{ "picture N \"filename\" \"alt-text\"", L"picture %d \"(%q*)\" \"(%q*)\"", OPS_1NUMBER_2TEXTS, FALSE },
{ "placeholder [name] = \"text\"", L"placeholder %[(%C+)%] = \"(%q*)\"", OPS_2TEXT, FALSE },
{ "project folder \"pathname\"", L"project folder \"(%q*)\"", OPS_1TEXT, FALSE },
{ "release \"text\"", L"release \"(%q*)\"", OPS_1TEXT, FALSE },
{ "release file \"filename\"", L"release file \"(%q*)\"", OPS_1TEXT, FALSE },
{ "release file \"filename\" from \"template\"",
L"release file \"(%q*)\" from \"(%q*)\"", OPS_2TEXT, FALSE },
{ "release source \"filename\" using \"filename\" from \"template\"",
L"release source \"(%q*)\" using \"(%q*)\" from \"(%q*)\"", OPS_3TEXT, FALSE },
{ "release to \"pathname\"", L"release to \"(%q*)\"", OPS_1TEXT, FALSE },
{ "resolution NxN max NxN", L"resolution (%d+) max (%d+)", OPS_2NUMBER, TRUE },
{ "resolution NxN min NxN max NxN", L"resolution (%d+) min (%d+) max (%d+)", OPS_3NUMBER, TRUE },
{ "resolution NxN min NxN", L"resolution (%d+) min (%d+)", OPS_2NUMBER, TRUE },
{ "resolution NxN", L"resolution (%d+)", OPS_1NUMBER, TRUE },
{ "solution", L"solution", OPS_NO, FALSE },
{ "solution public", L"solution public", OPS_NO, FALSE },
{ "sound ID \"filename\" music", L"sound (%i+) \"(%q*)\" music", OPS_2TEXT, TRUE },
{ "sound ID \"filename\" repeat N",
L"sound (%i+) \"(%q*)\" repeat (%d+)", OPS_2TEXT_1NUMBER, TRUE },
{ "sound ID \"filename\" repeat forever",
L"sound (%i+) \"(%q*)\" repeat forever", OPS_2TEXT, TRUE },
{ "sound ID \"filename\" song", L"sound (%i+) \"(%q*)\" song", OPS_2TEXT, TRUE },
{ "sound N \"filename\"", L"sound (%d+) \"(%q*)\"", OPS_1NUMBER_1TEXT, FALSE },
{ "sound ID \"filename\"", L"sound (%i+) \"(%q*)\"", OPS_2TEXT, FALSE },
{ "sound \"filename\"", L"sound \"(%q*)\"", OPS_1TEXT, FALSE },
{ "sound N \"filename\" \"alt-text\"", L"sound (%d+) \"(%q*)\" \"(%q*)\"", OPS_1NUMBER_2TEXTS, FALSE },
{ "source", L"source", OPS_NO, FALSE },
{ "source public", L"source public", OPS_NO, FALSE },
{ "status \"template\" \"filename\"", L"status \"(%q*)\" \"(%q*)\"", OPS_2TEXT, FALSE },
{ "status alternative ||link to Inform documentation||",
L"status alternative ||(%c*)||", OPS_1TEXT, FALSE },
{ "status instruction ||link to Inform source text||",
L"status instruction ||(%c*)||", OPS_1TEXT, FALSE },
{ "storyfile \"filename\" include", L"storyfile \"(%q*)\" include", OPS_1TEXT, FALSE },
{ "storyfile \"filename\"", L"storyfile \"(%q*)\"", OPS_1TEXT, TRUE },
{ "storyfile leafname \"leafname\"", L"storyfile leafname \"(%q*)\"", OPS_1TEXT, FALSE },
{ "template path \"folder\"", L"template path \"(%q*)\"", OPS_1TEXT, FALSE },
{ "website \"template\"", L"website \"(%q*)\"", OPS_1TEXT, FALSE },
{ NULL, NULL, OPS_NO, FALSE }
};
@h Summary.
For the |-help| information:
=
void Parser::summarise_blurb(void) {
PRINT("\nThe blurbfile is a script of commands, one per line, in these forms:\n");
for (int t=0; syntaxes[t].prototype; t++)
if (syntaxes[t].deprecated == FALSE)
PRINT(" %s\n", syntaxes[t].explicated);
PRINT("\nThe following syntaxes, though legal in Blorb 2001, are not supported:\n");
for (int t=0; syntaxes[t].prototype; t++)
if (syntaxes[t].deprecated == TRUE)
PRINT(" %s\n", syntaxes[t].explicated);
}
@h The interpreter.
The following routine is called for each line of the blurb file in sequence,
including any blank lines.
=
void Parser::interpret_line(text_stream *command, text_file_position *tf, void *state) {
BlorbErrors::set_error_position(tf);
match_results mr = Regexp::create_mr();
if (Regexp::match(&mr, command, L" *(%c*?) *")) Str::copy(command, mr.exp[0]);
if (Str::len(command) == 0) return; /* thus skip a line containing only blank space */
if (Str::get_first_char(command) == '!') return; /* thus skip a comment line */
if (trace_mode) PRINT("! %03d: %S\n", TextFiles::get_line_count(tf), command);
int num1 = 0, num2 = 0, num3 = 0, outcome = -1; /* which of the legal command syntaxes is used */
TEMPORARY_TEXT(text1);
TEMPORARY_TEXT(text2);
TEMPORARY_TEXT(text3);
@<Parse the command and set operands appropriately@>;
@<Take action on the command@>;
DISCARD_TEXT(text1);
DISCARD_TEXT(text2);
DISCARD_TEXT(text3);
Regexp::dispose_of(&mr);
}
@ Here we set |outcome| to the index in the syntaxes table of the line matched,
or leave it as $-1$ if no match can be made. Text and number operands are
copied in |text1|, |num1|, ..., accordingly.
@<Parse the command and set operands appropriately@> =
for (int t=0; syntaxes[t].prototype; t++)
if (Regexp::match(&mr, command, syntaxes[t].prototype)) {
switch (syntaxes[t].operands) {
case OPS_NO: break;
case OPS_1TEXT: Str::copy(text1, mr.exp[0]); break;
case OPS_2TEXT: Str::copy(text1, mr.exp[0]);
Str::copy(text2, mr.exp[1]); break;
case OPS_2TEXT_1NUMBER: Str::copy(text1, mr.exp[0]);
Str::copy(text2, mr.exp[1]);
num1 = Str::atoi(mr.exp[2], 0); break;
case OPS_1NUMBER: num1 = Str::atoi(mr.exp[0], 0); break;
case OPS_2NUMBER: num1 = Str::atoi(mr.exp[0], 0);
num2 = Str::atoi(mr.exp[1], 0); break;
case OPS_1NUMBER_1TEXT: num1 = Str::atoi(mr.exp[0], 0);
Str::copy(text1, mr.exp[1]); break;
case OPS_1NUMBER_2TEXTS: num1 = Str::atoi(mr.exp[0], 0);
Str::copy(text1, mr.exp[1]);
Str::copy(text2, mr.exp[2]); break;
case OPS_1NUMBER_1TEXT_1NUMBER: num1 = Str::atoi(mr.exp[0], 0);
Str::copy(text1, mr.exp[1]);
num2 = Str::atoi(mr.exp[2], 0); break;
case OPS_3NUMBER: num1 = Str::atoi(mr.exp[0], 0);
num2 = Str::atoi(mr.exp[1], 0);
num3 = Str::atoi(mr.exp[2], 0); break;
case OPS_3TEXT: Str::copy(text1, mr.exp[0]);
Str::copy(text2, mr.exp[1]);
Str::copy(text3, mr.exp[2]); break;
default: internal_error("unknown operand type");
}
outcome = t; break;
}
if (outcome == -1) {
BlorbErrors::error_1S("not a valid blurb command", command);
return;
}
if (syntaxes[outcome].deprecated) {
BlorbErrors::error_1("this Blurb syntax is no longer supported",
syntaxes[outcome].explicated);
return;
}
@ The command is now fully parsed, and is one that we support. We can act.
@<Take action on the command@> =
switch (outcome) {
case author_COMMAND:
Placeholders::set_to(I"AUTHOR", text1, 0);
Writer::author_chunk(text1);
break;
case auxiliary_COMMAND: Links::create_auxiliary_file(text1, text2, text3); break;
case base64_COMMAND:
Requests::request_2(BASE64_REQ, text1, text2, FALSE); break;
case copyright_COMMAND: Writer::copyright_chunk(text1); break;
case cover_COMMAND: @<Declare which file is the cover art@>; break;
case css_COMMAND: use_css_code_styles = TRUE; break;
case ifiction_file_COMMAND: Writer::metadata_chunk(Filenames::from_text(text1)); break;
case ifiction_COMMAND: Requests::request_1(IFICTION_REQ, I"", TRUE); break;
case ifiction_public_COMMAND: Requests::request_1(IFICTION_REQ, I"", FALSE); break;
case interpreter_COMMAND:
Placeholders::set_to(I"INTERPRETERVMIS", text2, 0);
Requests::request_1(INTERPRETER_REQ, text1, FALSE); break;
case picture_COMMAND: Writer::picture_chunk(num1, Filenames::from_text(text1), I""); break;
case picture_text_COMMAND: Writer::picture_chunk_text(text1, Filenames::from_text(text2)); break;
case picture_noid_COMMAND: Writer::picture_chunk_text(I"", Filenames::from_text(text1)); break;
case picture_with_alt_text_COMMAND: Writer::picture_chunk(num1, Filenames::from_text(text1), text2); break;
case placeholder_COMMAND: Placeholders::set_to(text1, text2, 0); break;
case project_folder_COMMAND: project_folder = Pathnames::from_text(text1); break;
case release_COMMAND:
Placeholders::set_to_number(I"RELEASE", num1);
Writer::release_chunk(num1);
break;
case release_file_COMMAND: {
filename *to_release = Filenames::from_text(text1);
TEMPORARY_TEXT(leaf);
WRITE_TO(leaf, "%f", Filenames::get_leafname(to_release));
Requests::request_3(COPY_REQ, text1, leaf, I"--", FALSE); break;
DISCARD_TEXT(leaf);
}
case release_file_from_COMMAND:
Requests::request_2(RELEASE_FILE_REQ, text1, text2, FALSE); break;
case release_to_COMMAND:
release_folder = Pathnames::from_text(text1);
@<Make pathname placeholders in three different formats@>;
break;
case release_source_COMMAND:
Requests::request_3(RELEASE_SOURCE_REQ, text1, text2, text3, FALSE); break;
case solution_COMMAND: Requests::request_1(SOLUTION_REQ, I"", TRUE); break;
case solution_public_COMMAND: Requests::request_1(SOLUTION_REQ, I"", FALSE); break;
case sound_COMMAND: Writer::sound_chunk(num1, Filenames::from_text(text1), I""); break;
case sound_text_COMMAND: Writer::sound_chunk_text(text1, Filenames::from_text(text2)); break;
case sound_noid_COMMAND: Writer::sound_chunk_text(I"", Filenames::from_text(text1)); break;
case sound_with_alt_text_COMMAND: Writer::sound_chunk(num1, Filenames::from_text(text1), text2); break;
case source_COMMAND: Requests::request_1(SOURCE_REQ, I"", TRUE); break;
case source_public_COMMAND: Requests::request_1(SOURCE_REQ, I"", FALSE); break;
case status_COMMAND: status_template = Filenames::from_text(text1); status_file = Filenames::from_text(text2); break;
case status_alternative_COMMAND: Requests::request_1(ALTERNATIVE_REQ, text1, FALSE); break;
case status_instruction_COMMAND: Requests::request_1(INSTRUCTION_REQ, text1, FALSE); break;
case storyfile_include_COMMAND: Writer::executable_chunk(Filenames::from_text(text1)); break;
case storyfile_leafname_COMMAND: Placeholders::set_to(I"STORYFILE", text1, 0); break;
case template_path_COMMAND: Templates::new_path(Pathnames::from_text(text1)); break;
case website_COMMAND: Requests::request_1(WEBSITE_REQ, text1, FALSE); break;
default: BlorbErrors::error_1S("***", command); BlorbErrors::fatal("*** command unimplemented ***\n");
}
@ We only ever set the frontispiece as resource number 1, since Inform
has the assumption that the cover art is image number 1 built in.
@<Declare which file is the cover art@> =
Placeholders::set_to(I"BIGCOVER", text1, 0);
cover_exists = TRUE;
cover_is_in_JPEG_format = FALSE;
filename *cover_filename = Filenames::from_text(text1);
if (Filenames::guess_format(cover_filename) == FORMAT_PERHAPS_JPEG)
cover_is_in_JPEG_format = TRUE;
Writer::frontispiece_chunk(1);
if (Str::eq_wide_string(Filenames::get_leafname(cover_filename), L"DefaultCover.jpg"))
default_cover_used = TRUE;
Placeholders::set_to(I"SMALLCOVER", text1, 0);
@ Here, |text1| is the pathname of the Release folder. If we suppose that
Inblorb is being run from Inform, then this folder is a subfolder of the
Materials folder for an I7 project. It follows that we can obtain the
pathname to the Materials folder by trimming the leaf and the final separator.
That makes the |MATERIALSFOLDERPATH| placeholder. We then set |MATERIALSFOLDER|
to the name of the Materials folder, e.g., "Spaceman Spiff Materials".
However, we also need two variants on the pathname, one to be supplied to the
Javascript function |openUrl| and one to |fileUrl|. For platform dependency
reasons these need to be manipulated to deal with awkward characters.
@<Make pathname placeholders in three different formats@> =
pathname *Release = Pathnames::from_text(text1);
pathname *Materials = Pathnames::up(Release);
TEMPORARY_TEXT(as_txt);
WRITE_TO(as_txt, "%p", Materials);
Placeholders::set_to(I"MATERIALSFOLDERPATH", as_txt, 0);
DISCARD_TEXT(as_txt);
Placeholders::set_to(I"MATERIALSFOLDER",
Pathnames::directory_name(Materials), 0);
Parser::qualify_placeholder(
I"MATERIALSFOLDERPATHOPEN",
I"MATERIALSFOLDERPATHFILE",
I"MATERIALSFOLDERPATH");
@ And here that very "qualification" routine. The placeholder |original| contains
the pathname to a folder, a pathname which might contain spaces or backslashes,
and which needs to be quoted as a literal Javascript string supplied to
either the function |openUrl| or the function |fileUrl|. Depending on the
platform in use, this may entail escaping spaces or reversing slashes in the
pathname in order to make versions for these two functions to use.
=
void Parser::qualify_placeholder(text_stream *openUrl_path, text_stream *fileUrl_path,
text_stream *original) {
text_stream *OU = Placeholders::read(openUrl_path);
text_stream *FU = Placeholders::read(fileUrl_path);
LOOP_THROUGH_TEXT(P, original) {
int c = Str::get(P);
if (c == ' ') {
if (escape_openUrl) WRITE_TO(OU, "%%2520");
else PUT_TO(OU, c);
if (escape_fileUrl) WRITE_TO(FU, "%%2520");
else PUT_TO(FU, c);
} else if (c == '\\') {
if (reverse_slash_openUrl) PUT_TO(OU, '/');
else PUT_TO(OU, c);
if (reverse_slash_fileUrl) PUT_TO(FU, '/');
else PUT_TO(FU, c);
} else {
PUT_TO(OU, c);
PUT_TO(FU, c);
}
}
}