To convert an extension from the traditional one-file format to the more modern directory-based format.
§1. In all paths through this function, we must write either good news or bad to OUT: bad news usually following from the file system refusing to create a text file or directory.
void ExtensionConverter::go(inform_extension *E, text_stream *OUT) { Extensions::read_source_text_for(E); TEMPORARY_TEXT(dirname) Editions::write_canonical_leaf(dirname, E->as_copy->edition); WRITE_TO(dirname, ".i7xd"); pathname *P_home = Pathnames::down(Filenames::up(E->as_copy->location_if_file), dirname); pathname *P_source = Pathnames::down(P_home, I"Source"); pathname *P_documentation = Pathnames::down(P_home, I"Documentation"); pathname *P_examples = Pathnames::down(P_documentation, I"Examples"); Create the home directory for the extension1.1; Construct JSON metadata and write it as a file1.2; Write out the source code1.3; Write out the documentation1.4; WRITE("migrated to directory '%S'\n", dirname); DISCARD_TEXT(dirname) }
§1.1. Create the home directory for the extension1.1 =
if (Directories::exists(P_home)) { WRITE("can't make this into %S because directory '%p' already exists", dirname, P_home); return; } if (ExtensionConverter::mkdir(E, OUT, P_home) == FALSE) return;
- This code is used in §1.
§1.2. Construct JSON metadata and write it as a file1.2 =
JSON_value *JM = JSON::new_object(); JSON_value *is = JSON::new_object(); JSON::add_to_object(JM, I"is", is); JSON::add_to_object(is, I"type", JSON::new_string(I"extension")); JSON::add_to_object(is, I"title", JSON::new_string(E->as_copy->edition->work->title)); JSON::add_to_object(is, I"author", JSON::new_string(E->as_copy->edition->work->author_name)); semantic_version_number V = E->as_copy->edition->version; if (VersionNumbers::is_null(V) == FALSE) { TEMPORARY_TEXT(vt) WRITE_TO(vt, "%v", &V); JSON::add_to_object(is, I"version", JSON::new_string(vt)); DISCARD_TEXT(vt) } filename *JF = Filenames::in(P_home, I"extension_metadata.json"); text_stream JSONF_struct; text_stream *JS = &JSONF_struct; if (ExtensionConverter::fopen(E, OUT, JS, JF) == FALSE) return; JSON::encode(JS, JM); STREAM_CLOSE(JS);
- This code is used in §1.
§1.3. Write out the source code1.3 =
if (ExtensionConverter::mkdir(E, OUT, P_source) == FALSE) return; TEMPORARY_TEXT(sleaf) Editions::write_canonical_leaf(sleaf, E->as_copy->edition); WRITE_TO(sleaf, ".i7x"); filename *SF = Filenames::in(P_source, sleaf); DISCARD_TEXT(sleaf) text_stream SRCF_struct; text_stream *SS = &SRCF_struct; if (ExtensionConverter::fopen(E, OUT, SS, SF) == FALSE) return; WRITE_TO(SS, "%S", E->read_into_file->body_text); STREAM_CLOSE(SS);
- This code is used in §1.
§1.4. Write out the documentation1.4 =
text_stream *source = E->read_into_file->torn_off_documentation; if (Str::is_whitespace(source) == FALSE) { if (ExtensionConverter::mkdir(E, OUT, P_documentation) == FALSE) return; filename *F_documentation = Filenames::in(P_documentation, I"Documentation.md"); text_stream DOCF_struct; text_stream *S_documentation = &DOCF_struct; if (ExtensionConverter::fopen(E, OUT, S_documentation, F_documentation) == FALSE) return; Filter the documentation through1.4.1; STREAM_CLOSE(S_documentation); }
- This code is used in §1.
§1.4.1. We work through the documentation attached to the original extension (if there was any) in two passes. On pass 1, we do nothing except to count the number of section and chapter headings. On pass 2, we split up the content into the main file and individual example files.
Filter the documentation through1.4.1 =
int chapter_count = 0, section_count = 0, example_count = 0; text_stream EG_struct; for (int pass = 1; pass <= 2; pass++) { text_stream *dest = S_documentation, *S_example = NULL; TEMPORARY_TEXT(line) int indentation = 0, space_count = 0; for (int i=0; i<Str::len(source); i++) { inchar32_t c = Str::get_at(source, i); if (c == '\n') { Line read1.4.1.1; Str::clear(line); indentation = 0; space_count = 0; } else if ((Str::len(line) == 0) && (Characters::is_whitespace(c))) { if (c == '\t') indentation++; if (c == ' ') space_count++; if (space_count == 4) { indentation++; space_count = 0; } } else { PUT_TO(line, c); } } if (Str::len(line) > 0) Line read1.4.1.1; DISCARD_TEXT(line) if (pass == 2) End any example1.4.1.2; }
- This code is used in §1.4.
Str::trim_white_space(line); match_results mr = Regexp::create_mr(); if ((Regexp::match(&mr, line, U"Section *: *(%c+?)")) || (Regexp::match(&mr, line, U"Section *- *(%c+?)"))) { if (pass == 1) section_count++; if (pass == 2) End any example1.4.1.2; } else if ((Regexp::match(&mr, line, U"Chapter *: *(%c+?)")) || (Regexp::match(&mr, line, U"Chapter *- *(%c+?)"))) { if (pass == 1) chapter_count++; if (pass == 2) { End any example1.4.1.2; Amend this to a section heading1.4.1.1.1; } } if (pass == 2) { if ((Regexp::match(&mr, line, U"Example *: *(%**) *(%c+?)")) || (Regexp::match(&mr, line, U"Example *- *(%**) *(%c+?)"))) Deal with an example heading1.4.1.1.2 else Copy the line out to the appropriate file1.4.1.1.3; } Regexp::dispose_of(&mr);
- This code is used in §1.4.1 (twice).
§1.4.1.1.1. Old single-file extensions tended to use Chapters, intended as major headings, with not much content, and not to use Sections at all. Because we now split off Chapters into their own HTML pages, we don't want that, so when converting an old extension which has chapters but no sections, we downgrade all the chapters to sections.
Amend this to a section heading1.4.1.1.1 =
if (section_count == 0) { Str::clear(line); WRITE_TO(line, "Section: %S", mr.exp[0]); }
- This code is used in §1.4.1.1.
§1.4.1.1.2. Deal with an example heading1.4.1.1.2 =
text_stream *stars = mr.exp[0]; text_stream *title = mr.exp[1]; text_stream *desc = NULL; match_results mr2 = Regexp::create_mr(); if (Regexp::match(&mr2, title, U" *(%c+?) - *(%c+) *")) { title = mr2.exp[0]; desc = mr2.exp[1]; } Begin an example1.4.1.1.2.1; Regexp::dispose_of(&mr2);
- This code is used in §1.4.1.1.
§1.4.1.1.2.1. Begin an example1.4.1.1.2.1 =
if ((example_count++ == 0) && (ExtensionConverter::mkdir(E, OUT, P_examples) == FALSE)) return; TEMPORARY_TEXT(eleaf) for (int i=0, last_was_ws=TRUE; i<Str::len(title); i++) { inchar32_t c = Str::get_at(title, i); if (Characters::is_whitespace(c)) { last_was_ws = TRUE; continue; } if (last_was_ws) c = Characters::toupper(c); last_was_ws = FALSE; if ((c == '.') || (c == ',') || (c == ';') || (c == '"') || (c == '\'')) continue; PUT_TO(eleaf, c); } WRITE_TO(eleaf, ".txt"); filename *F_example = Filenames::in(P_examples, eleaf); DISCARD_TEXT(eleaf) S_example = &EG_struct; if (ExtensionConverter::fopen(E, OUT, S_example, F_example) == FALSE) return; dest = S_example; WRITE_TO(dest, "Example: %S %S\n", stars, title); if (Str::len(desc) > 0) WRITE_TO(dest, "Description: %S\n", desc);
- This code is used in §1.4.1.1.2.
§1.4.1.2. End any example1.4.1.2 =
if (S_example) { STREAM_CLOSE(S_example); S_example = NULL; dest = S_documentation; }
§1.4.1.1.3. Note that we amend the old-style paste marker to the new style, though both are legal.
Copy the line out to the appropriate file1.4.1.1.3 =
for (int i=0; i<indentation; i++) PUT_TO(dest, '\t'); match_results mr2 = Regexp::create_mr(); if ((indentation == 1) && (Regexp::match(&mr2, line, U"%* *: *(%c+?)"))) { WRITE_TO(dest, "{*}%S\n", mr2.exp[0]); } else { WRITE_TO(dest, "%S\n", line); } Regexp::dispose_of(&mr2);
- This code is used in §1.4.1.1.
§2. And this provides bad news texts when the two main file-system operations fail.
int ExtensionConverter::mkdir(inform_extension *E, text_stream *OUT, pathname *P) { if (Pathnames::create_in_file_system(P) == FALSE) { WRITE("unable to create directory '%p'", P); return FALSE; } return TRUE; } int ExtensionConverter::fopen(inform_extension *E, text_stream *OUT, text_stream *S, filename *F) { if (STREAM_OPEN_TO_FILE(S, F, UTF8_ENC) == FALSE) { WRITE("unable to create file '%f'", F); return FALSE; } return TRUE; }