2020-05-09 14:10:43 +03:00
|
|
|
[ExtensionWebsite::] The Mini-Website.
|
|
|
|
|
|
|
|
To refresh the mini-website of available extensions presented in the
|
|
|
|
Inform GUI applications.
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
@ The Inform GUI apps present HTML in-app documentation on extensions: in
|
2020-05-09 14:10:43 +03:00
|
|
|
effect, a mini-website showing all the extensions available to the current
|
|
|
|
user, and giving detailed documentation on each one. The code in this
|
|
|
|
chapter of //supervisor// runs only if and when we want to generate or
|
|
|
|
update that website, and plays no part in Inform compilation or building
|
|
|
|
as such: it lives in //supervisor// because it's essentially concerned
|
|
|
|
with managing resources (i.e., extensions in nests).
|
|
|
|
|
|
|
|
A principle used throughout is that we fail safe and silent: if we can't
|
|
|
|
write the documentation website for any reason (permissions failures, for
|
|
|
|
example) then we make no complaint. It's a convenience for the user, but not
|
|
|
|
an essential. This point of view was encouraged by many Inform users working
|
|
|
|
clandestinely on thumb drives at their places of work, and whose employers
|
|
|
|
had locked their computers down fairly heavily.
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
@ The site has a very simple structure: there is an index page, and then
|
|
|
|
each visible extension is given its own page(s) concerning that extension alone.
|
2020-05-09 14:10:43 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
Note that the "census" gives us a list of all extensions normally visible
|
|
|
|
to the project: those it has installed, and those built into the app. But
|
|
|
|
the project might also still be using an extension from the legacy external
|
|
|
|
area, so we have to document everything it uses as well as everything in
|
|
|
|
the census, to be on the safe side.
|
2020-05-09 14:10:43 +03:00
|
|
|
|
|
|
|
=
|
2023-07-13 02:23:12 +03:00
|
|
|
void ExtensionWebsite::update(inform_project *proj) {
|
|
|
|
LOGIF(EXTENSIONS_CENSUS, "Updating extensions documentation for project\n");
|
2020-05-10 19:33:41 +03:00
|
|
|
HTML::set_link_abbreviation_path(Projects::path(proj));
|
2020-05-09 14:10:43 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
inform_extension *E;
|
|
|
|
LOOP_OVER_LINKED_LIST(E, inform_extension, proj->extensions_included)
|
2023-07-20 01:46:39 +03:00
|
|
|
ExtensionWebsite::document_extension(E, proj);
|
2020-05-09 14:10:43 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
linked_list *census = Projects::perform_census(proj);
|
|
|
|
inbuild_search_result *res;
|
|
|
|
LOOP_OVER_LINKED_LIST(res, inbuild_search_result, census)
|
2023-07-20 01:46:39 +03:00
|
|
|
ExtensionWebsite::document_extension(Extensions::from_copy(res->copy), proj);
|
2023-07-23 13:39:06 +03:00
|
|
|
|
|
|
|
ExtensionIndex::write(proj);
|
2020-05-09 14:10:43 +03:00
|
|
|
}
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
@ The top-level index page is at this filename.
|
2020-05-09 14:10:43 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
The distinction between these two calls is that |ExtensionWebsite::index_page_filename|
|
|
|
|
returns just the filename, and produces |NULL| only if there is no materials folder,
|
|
|
|
which certainly means we wouldn't want to be writing documentation to it;
|
|
|
|
but |ExtensionWebsite::cut_way_for_index_page| cuts its way through the file-system
|
|
|
|
with a machete in order to ensure that its parent directory will indeed exist.
|
|
|
|
That returns |NULL| if this fails because e.g. the file system objects.
|
2020-05-09 14:10:43 +03:00
|
|
|
|
|
|
|
=
|
2023-07-13 02:23:12 +03:00
|
|
|
filename *ExtensionWebsite::index_page_filename(inform_project *proj) {
|
|
|
|
if (proj == NULL) internal_error("no project");
|
|
|
|
pathname *P = ExtensionWebsite::path_to_site(proj, FALSE, FALSE);
|
|
|
|
if (P == NULL) return NULL;
|
|
|
|
return Filenames::in(P, I"Extensions.html");
|
2020-05-09 14:10:43 +03:00
|
|
|
}
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
filename *ExtensionWebsite::cut_way_for_index_page(inform_project *proj) {
|
|
|
|
if (proj == NULL) internal_error("no project");
|
|
|
|
pathname *P = ExtensionWebsite::path_to_site(proj, FALSE, TRUE);
|
2020-05-09 14:10:43 +03:00
|
|
|
if (P == NULL) return NULL;
|
2023-07-13 02:23:12 +03:00
|
|
|
return Filenames::in(P, I"Extensions.html");
|
2020-05-09 14:10:43 +03:00
|
|
|
}
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
@ And this finds, or if |use_machete| is set, also makes way for, the directory
|
|
|
|
in which our mini-website is to be built.
|
2020-05-09 14:10:43 +03:00
|
|
|
|
|
|
|
=
|
2023-07-13 02:23:12 +03:00
|
|
|
pathname *ExtensionWebsite::path_to_site(inform_project *proj, int relative, int use_machete) {
|
|
|
|
if (relative) use_machete = FALSE; /* just for safety's sake */
|
|
|
|
pathname *P = NULL;
|
|
|
|
if (relative == FALSE) {
|
|
|
|
if (proj == NULL) internal_error("no project");
|
2023-04-20 01:28:00 +03:00
|
|
|
P = Projects::materials_path(proj);
|
|
|
|
if (P == NULL) return NULL;
|
|
|
|
}
|
2023-07-13 02:23:12 +03:00
|
|
|
P = Pathnames::down(P, I"Extensions");
|
|
|
|
if ((use_machete) && (Pathnames::create_in_file_system(P) == 0)) return NULL;
|
|
|
|
P = Pathnames::down(P, I"Reserved");
|
|
|
|
if ((use_machete) && (Pathnames::create_in_file_system(P) == 0)) return NULL;
|
|
|
|
P = Pathnames::down(P, I"Documentation");
|
|
|
|
if ((use_machete) && (Pathnames::create_in_file_system(P) == 0)) return NULL;
|
|
|
|
return P;
|
2020-05-09 14:10:43 +03:00
|
|
|
}
|
2023-07-04 23:49:18 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
@ And similarly for pages which hold individual extension documentation. Note
|
|
|
|
that if |eg_number| is positive, it should be 1, 2, 3, ... up to the number of
|
|
|
|
examples provided in the extension.
|
|
|
|
|
|
|
|
=
|
|
|
|
filename *ExtensionWebsite::page_filename(inform_project *proj, inbuild_edition *edition,
|
2023-07-04 23:49:18 +03:00
|
|
|
int eg_number) {
|
2023-07-13 02:23:12 +03:00
|
|
|
if (proj == NULL) internal_error("no project");
|
|
|
|
return ExtensionWebsite::page_filename_inner(proj, edition, eg_number, FALSE, FALSE);
|
|
|
|
}
|
2023-07-04 23:49:18 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
filename *ExtensionWebsite::page_filename_relative_to_materials(inbuild_edition *edition,
|
|
|
|
int eg_number) {
|
|
|
|
return ExtensionWebsite::page_filename_inner(NULL, edition, eg_number, TRUE, FALSE);
|
|
|
|
}
|
2023-07-04 23:49:18 +03:00
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
filename *ExtensionWebsite::cut_way_for_page(inform_project *proj,
|
|
|
|
inbuild_edition *edition, int eg_number) {
|
|
|
|
if (proj == NULL) internal_error("no project");
|
|
|
|
return ExtensionWebsite::page_filename_inner(proj, edition, eg_number, FALSE, TRUE);
|
2023-07-04 23:49:18 +03:00
|
|
|
}
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
@ All of which use this private utility function:
|
|
|
|
|
|
|
|
=
|
|
|
|
filename *ExtensionWebsite::page_filename_inner(inform_project *proj, inbuild_edition *edition,
|
|
|
|
int eg_number, int relative, int use_machete) {
|
|
|
|
if (relative) use_machete = FALSE; /* just for safety's sake */
|
2023-07-04 23:49:18 +03:00
|
|
|
TEMPORARY_TEXT(leaf)
|
|
|
|
Editions::write_canonical_leaf(leaf, edition);
|
|
|
|
|
2023-07-13 02:23:12 +03:00
|
|
|
pathname *P = ExtensionWebsite::path_to_site(proj, relative, use_machete);
|
|
|
|
if (P == NULL) return NULL;
|
2023-07-04 23:49:18 +03:00
|
|
|
P = Pathnames::down(P, edition->work->author_name);
|
2023-07-13 02:23:12 +03:00
|
|
|
if ((use_machete) && (Pathnames::create_in_file_system(P) == 0)) return NULL;
|
2023-07-04 23:49:18 +03:00
|
|
|
P = Pathnames::down(P, leaf);
|
2023-07-13 02:23:12 +03:00
|
|
|
if ((use_machete) && (Pathnames::create_in_file_system(P) == 0)) return NULL;
|
2023-07-04 23:49:18 +03:00
|
|
|
Str::clear(leaf);
|
|
|
|
if (eg_number > 0) WRITE_TO(leaf, "eg%d.html", eg_number);
|
|
|
|
else WRITE_TO(leaf, "index.html");
|
2023-07-13 02:23:12 +03:00
|
|
|
|
2023-07-04 23:49:18 +03:00
|
|
|
filename *F = Filenames::in(P, leaf);
|
|
|
|
DISCARD_TEXT(leaf)
|
|
|
|
return F;
|
|
|
|
}
|
2023-07-13 02:23:12 +03:00
|
|
|
|
2023-07-17 02:44:11 +03:00
|
|
|
@ And this is where extension documentation is kicked off.
|
|
|
|
|
|
|
|
=
|
2023-07-20 01:46:39 +03:00
|
|
|
void ExtensionWebsite::document_extension(inform_extension *E, inform_project *proj) {
|
2023-07-17 02:44:11 +03:00
|
|
|
if (E == NULL) internal_error("no extension");
|
|
|
|
if (proj == NULL) internal_error("no project");
|
|
|
|
if (E->documented_on_this_run) return;
|
|
|
|
inbuild_edition *edition = E->as_copy->edition;
|
|
|
|
inbuild_work *work = edition->work;
|
|
|
|
if (LinkedLists::len(E->as_copy->errors_reading_source_text) > 0) {
|
|
|
|
LOG("Not writing documentation on %X because it has copy errors\n", work);
|
|
|
|
} else {
|
|
|
|
LOG("Writing documentation on %X\n", work);
|
|
|
|
inbuild_edition *edition = E->as_copy->edition;
|
|
|
|
filename *F = ExtensionWebsite::cut_way_for_page(proj, edition, -1);
|
|
|
|
if (F == NULL) return;
|
|
|
|
pathname *P = Filenames::up(F);
|
|
|
|
if (Pathnames::create_in_file_system(P) == 0) return;
|
2023-07-18 00:54:02 +03:00
|
|
|
compiled_documentation *doc = Extensions::get_documentation(E);
|
|
|
|
TEMPORARY_TEXT(OUT)
|
2023-07-17 02:44:11 +03:00
|
|
|
#ifdef CORE_MODULE
|
2023-07-18 00:54:02 +03:00
|
|
|
TEMPORARY_TEXT(details)
|
2023-07-17 02:44:11 +03:00
|
|
|
IndexExtensions::document_in_detail(details, E);
|
2023-07-18 00:54:02 +03:00
|
|
|
if (Str::len(details) > 0) {
|
|
|
|
HTML_TAG("hr"); /* ruled line at top of extras */
|
|
|
|
HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
|
|
|
|
WRITE("defined these in the last run");
|
|
|
|
HTML_CLOSE("p");
|
|
|
|
HTML_OPEN("div");
|
|
|
|
WRITE("%S", details);
|
2023-08-01 01:33:58 +03:00
|
|
|
HTML_CLOSE("div");
|
2023-07-18 00:54:02 +03:00
|
|
|
}
|
2023-08-01 01:33:58 +03:00
|
|
|
if (E->as_copy->edition->work->genre == extension_bundle_genre)
|
|
|
|
@<Add internals@>;
|
2023-07-18 00:54:02 +03:00
|
|
|
DISCARD_TEXT(details)
|
2023-07-17 02:44:11 +03:00
|
|
|
#endif
|
2023-07-18 00:54:02 +03:00
|
|
|
DocumentationRenderer::as_HTML(P, doc, OUT);
|
|
|
|
DISCARD_TEXT(OUT)
|
2023-07-17 02:44:11 +03:00
|
|
|
}
|
|
|
|
E->documented_on_this_run = TRUE;
|
|
|
|
}
|
2023-07-18 00:54:02 +03:00
|
|
|
|
2023-08-01 01:33:58 +03:00
|
|
|
@<Add internals@> =
|
|
|
|
HTML_TAG("hr"); /* ruled line at top of extras */
|
|
|
|
HTML_OPEN_WITH("p", "class=\"extensionsubheading\"");
|
|
|
|
WRITE("internals");
|
|
|
|
HTML_CLOSE("p");
|
|
|
|
HTML_OPEN("p");
|
|
|
|
WRITE("Metadata in JSON form ");
|
|
|
|
HTML_OPEN_WITH("a", "href=\"metadata.html\" class=\"registrycontentslink\"");
|
|
|
|
WRITE("can be read here");
|
|
|
|
HTML_CLOSE("a");
|
|
|
|
WRITE(".");
|
|
|
|
text_stream *MD = DocumentationRenderer::open_subpage(P, I"metadata.html");
|
|
|
|
TEMPORARY_TEXT(title)
|
|
|
|
WRITE_TO(title, "%X", E->as_copy->edition->work);
|
|
|
|
DocumentationRenderer::render_header(MD, title, I"Metadata", NULL);
|
|
|
|
DISCARD_TEXT(title)
|
|
|
|
ExtensionWebsite::write_metadata_page(MD, E);
|
|
|
|
linked_list *L = NEW_LINKED_LIST(inbuild_nest);
|
|
|
|
inbuild_nest *N = Extensions::materials_nest(E);
|
|
|
|
ADD_TO_LINKED_LIST(N, inbuild_nest, L);
|
|
|
|
inbuild_requirement *req;
|
|
|
|
LOOP_OVER_LINKED_LIST(req, inbuild_requirement, E->kits) {
|
|
|
|
inform_kit *K = Kits::find_by_name(req->work->raw_title, L, NULL);
|
|
|
|
if (K) @<Link to documentation on K@>;
|
|
|
|
}
|
|
|
|
HTML_CLOSE("p");
|
|
|
|
DocumentationRenderer::close_subpage();
|
|
|
|
LOOP_OVER_LINKED_LIST(req, inbuild_requirement, E->kits) {
|
|
|
|
inform_kit *K = Kits::find_by_name(req->work->raw_title, L, NULL);
|
|
|
|
if (K) @<Create documentation on K@>;
|
|
|
|
}
|
|
|
|
|
|
|
|
@<Link to documentation on K@> =
|
|
|
|
WRITE(" This extension includes the kit ");
|
|
|
|
HTML_OPEN_WITH("a", "href=\"%S/index.html\" class=\"registrycontentslink\"",
|
|
|
|
K->as_copy->edition->work->title);
|
|
|
|
WRITE("%S", K->as_copy->edition->work->title);
|
|
|
|
HTML_CLOSE("a");
|
|
|
|
WRITE(".");
|
|
|
|
|
|
|
|
@<Create documentation on K@> =
|
|
|
|
pathname *KP = Pathnames::down(P, req->work->raw_title);
|
|
|
|
if (Pathnames::create_in_file_system(KP)) {
|
|
|
|
pathname *KD = Pathnames::down(K->as_copy->location_if_path, I"Documentation");
|
|
|
|
compiled_documentation *doc =
|
|
|
|
DocumentationCompiler::compile_from_path(KD, NULL);
|
|
|
|
if (doc == NULL) {
|
|
|
|
text_stream *OUT = DocumentationRenderer::open_subpage(KP, I"index.html");
|
|
|
|
DocumentationRenderer::render_header(OUT, K->as_copy->edition->work->title, NULL, E);
|
|
|
|
HTML_OPEN("p");
|
|
|
|
WRITE("The kit %S does not provide any internal documentation.",
|
|
|
|
K->as_copy->edition->work->title);
|
|
|
|
HTML_CLOSE("p");
|
|
|
|
DocumentationRenderer::close_subpage();
|
|
|
|
} else {
|
|
|
|
doc->within_extension = E;
|
|
|
|
DocumentationRenderer::as_HTML(KP, doc, NULL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@ =
|
|
|
|
void ExtensionWebsite::write_metadata_page(OUTPUT_STREAM, inform_extension *E) {
|
|
|
|
if (E->as_copy->metadata_record) {
|
|
|
|
HTML_OPEN("p");
|
|
|
|
WRITE("Metadata for extensions, that is, the detail of what they are and "
|
|
|
|
"what they need, is stored in an Internet-standard format called JSON. "
|
|
|
|
"The following is the metadata on %X:", E->as_copy->edition->work);
|
|
|
|
HTML_CLOSE("p");
|
|
|
|
HTML_OPEN("pre");
|
|
|
|
JSON::encode(OUT, E->as_copy->metadata_record);
|
|
|
|
HTML_CLOSE("pre");
|
|
|
|
} else {
|
|
|
|
HTML_OPEN("p");
|
|
|
|
WRITE("For some reason, no JSON metadata is available for this extension.");
|
|
|
|
HTML_CLOSE("p");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-18 00:54:02 +03:00
|
|
|
text_stream *EXW_breadcrumb_titles[5] = { NULL, NULL, NULL, NULL, NULL };
|
|
|
|
text_stream *EXW_breakcrumb_URLs[5] = { NULL, NULL, NULL, NULL, NULL };
|
|
|
|
int no_EXW_breadcrumbs = 0;
|
|
|
|
|
|
|
|
void ExtensionWebsite::add_home_breadcrumb(text_stream *title) {
|
|
|
|
if (title == NULL) title = I"Extensions";
|
|
|
|
ExtensionWebsite::add_breadcrumb(title,
|
|
|
|
I"inform:/Extensions/Reserved/Documentation/Extensions.html");
|
|
|
|
}
|
|
|
|
|
|
|
|
void ExtensionWebsite::add_breadcrumb(text_stream *title, text_stream *URL) {
|
|
|
|
if (no_EXW_breadcrumbs >= 5) internal_error("too many breadcrumbs");
|
|
|
|
EXW_breadcrumb_titles[no_EXW_breadcrumbs] = Str::duplicate(title);
|
|
|
|
EXW_breakcrumb_URLs[no_EXW_breadcrumbs] = Str::duplicate(URL);
|
|
|
|
no_EXW_breadcrumbs++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ExtensionWebsite::titling_and_navigation(OUTPUT_STREAM, text_stream *subtitle) {
|
|
|
|
HTML_OPEN_WITH("div", "class=\"headingpanellayout headingpanelalt\"");
|
|
|
|
HTML_OPEN_WITH("div", "class=\"headingtext\"");
|
|
|
|
HTML::begin_span(OUT, I"headingpaneltextalt");
|
|
|
|
for (int i=0; i<no_EXW_breadcrumbs; i++) {
|
|
|
|
if (i>0) WRITE(" > ");
|
|
|
|
if ((i != no_EXW_breadcrumbs-1) && (Str::len(EXW_breakcrumb_URLs[i]) > 0)) {
|
|
|
|
HTML_OPEN_WITH("a", "href=\"%S\" class=\"registrycontentslink\"", EXW_breakcrumb_URLs[i]);
|
|
|
|
}
|
|
|
|
DocumentationRenderer::render_text(OUT, EXW_breadcrumb_titles[i]);
|
|
|
|
if ((i != no_EXW_breadcrumbs-1) && (Str::len(EXW_breakcrumb_URLs[i]) > 0)) {
|
|
|
|
HTML_CLOSE("a");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
HTML::end_span(OUT);
|
|
|
|
HTML_CLOSE("div");
|
|
|
|
HTML_OPEN_WITH("div", "class=\"headingrubric\"");
|
|
|
|
HTML::begin_span(OUT, I"headingpanelrubricalt");
|
|
|
|
DocumentationRenderer::render_text(OUT, subtitle);
|
|
|
|
HTML::end_span(OUT);
|
|
|
|
HTML_CLOSE("div");
|
|
|
|
HTML_CLOSE("div");
|
|
|
|
no_EXW_breadcrumbs = 0;
|
|
|
|
}
|
2023-07-21 13:49:36 +03:00
|
|
|
|
|
|
|
@ This is a new-look paste button, using a "command-V" ideograph rather than
|
|
|
|
a somewhat enigmatic icon.
|
|
|
|
|
|
|
|
=
|
|
|
|
void ExtensionWebsite::paste_button(OUTPUT_STREAM, text_stream *matter) {
|
|
|
|
TEMPORARY_TEXT(paste)
|
|
|
|
ExtensionWebsite::paste_ideograph(paste);
|
|
|
|
PasteButtons::paste_text_using(OUT, matter, paste);
|
|
|
|
DISCARD_TEXT(paste)
|
|
|
|
WRITE(" ");
|
|
|
|
}
|
|
|
|
void ExtensionWebsite::paste_ideograph(OUTPUT_STREAM) {
|
|
|
|
/* the Unicode for "place of interest", the Swedish castle which became the Apple action symbol */
|
|
|
|
WRITE("<span class=\"paste\">%cV</span>", 0x2318);
|
|
|
|
}
|