diff --git a/css/components/elements/_misc-content.scss b/css/components/elements/_misc-content.scss index 7a43c8bcbb..83265a4cfc 100644 --- a/css/components/elements/_misc-content.scss +++ b/css/components/elements/_misc-content.scss @@ -151,6 +151,16 @@ article.theorem-like .emphasis { font-style: oblique; } +.preview-build-warning { + background-color: rgb(249, 240, 240); + border: 2px solid rgb(202, 38, 38); + border-radius: 2px; + color: #333; + padding: 10px; + margin: 10px 0; +} + + /* Adapted from William Hammond (attributed to David Carlisle) */ /* "mathjax-users" Google Group, 2015-12-27 */ diff --git a/pretext/lib/pretext.py b/pretext/lib/pretext.py index 502de99caa..4ed42ae502 100644 --- a/pretext/lib/pretext.py +++ b/pretext/lib/pretext.py @@ -91,6 +91,8 @@ # contextmanager tools import contextlib +import time + # cleanup multiline strings used as source code import textwrap @@ -4253,6 +4255,12 @@ def _parse_runestone_services(et): return (rs_js, rs_css, rs_cdn_url, rs_version) +# Update stringparams with Runestone Services information +def _set_runestone_stringparams(stringparams, rs_js, rs_css, rs_version): + stringparams["rs-js"] = rs_js + stringparams["rs-css"] = rs_css + stringparams["rs-version"] = rs_version + # A helper function to query the latest Runestone # Services file, while failing gracefully @@ -4298,15 +4306,8 @@ def _runestone_services(stringparams, ext_rs_methods): # Developer is responsible for placement of the right files in _static # ** Simply return early with stock values (or None) ** if "debug.rs.dev" in stringparams: - rs_js = "prefix-runtime.bundle.js:prefix-runtime-libs.bundle.js:prefix-runestone.bundle.js" - rs_css = "prefix-runtime-libs.css:prefix-runestone.css" - rs_cdn_url = None - rs_version = "dev" - services_xml = None - # Return, plus side-effect - stringparams["rs-js"] = rs_js - stringparams["rs-css"] = rs_css - stringparams["rs-version"] = rs_version + rs_js, rs_css, rs_cdn_url, rs_version, services_xml = _runestone_debug_service_info() + _set_runestone_stringparams(stringparams, rs_js, rs_css, rs_version) return (rs_js, rs_css, rs_cdn_url, rs_version, services_xml) # Otherwise, we have a URL pointing to the Runestone server/CDN @@ -4333,11 +4334,17 @@ def _runestone_services(stringparams, ext_rs_methods): rs_js, rs_css, rs_cdn_url, rs_version = _parse_runestone_services(services) # Return, plus side-effect - stringparams["rs-js"] = rs_js - stringparams["rs-css"] = rs_css - stringparams["rs-version"] = rs_version + _set_runestone_stringparams(stringparams, rs_js, rs_css, rs_version) return (rs_js, rs_css, rs_cdn_url, rs_version, services_xml) +def _runestone_debug_service_info(): + """Return hardcoded values used for debugging Runestone Services (debug.rs.dev)""" + rs_js = "prefix-runtime.bundle.js:prefix-runtime-libs.bundle.js:prefix-runestone.bundle.js" + rs_css = "prefix-runtime-libs.css:prefix-runestone.css" + rs_cdn_url = None + rs_version = "dev" + services_xml = None + return (rs_js, rs_css, rs_cdn_url, rs_version, services_xml) def _cdn_runestone_services(stringparams, ext_rs_methods): """Version of _runestone_services function to query the Runestone Services file from the PreTeXt html-static CDN""" @@ -4442,6 +4449,21 @@ def query_runestone_services(services_url): return services_response.text +def query_existing_runestone_services(dest_dir, stringparams): + '''Attempt to get Runestone service data from existing + Runestone Services file in _static directory. + Returns a tuple of the JS, CSS, CDN URL and version or None''' + services_record_files = os.path.join(dest_dir, "_static", "_runestone-services.xml") + + if os.path.exists(services_record_files): + with open(services_record_files, 'r') as f: + services_xml = f.read() + services = ET.fromstring(services_xml) + return _parse_runestone_services(services) + else: + msg = "query_existing_runestone_services failed: no _runestone-services.xml file found in _static directory" + raise RuntimeError(msg) + def _place_runestone_services(tmp_dir, stringparams, ext_rs_methods): '''Obtain Runestone Services and place in _static directory of build''' @@ -4715,6 +4737,9 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi # to ensure provided stringparams aren't mutated unintentionally stringparams = stringparams.copy() + log_time_info = stringparams.get("profile-py", False) == "yes" + time_logger = Stopwatch("html()", log_time_info) + # Consult publisher file for locations of images generated_abs, external_abs = get_managed_directories(xml, pub_file) # Consult source for additional files @@ -4725,6 +4750,7 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi pub_vars = get_publisher_variable_report(xml, pub_file, stringparams) include_static_files = get_publisher_variable(pub_vars, 'portable-html') != "yes" + time_logger.log("pubvars loaded") if include_static_files: # interrogate Runestone server (or debugging switches) and populate @@ -4734,6 +4760,7 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi # even if we don't need static files, we need to set stringparams for # Runestone Services information. _cdn_runestone_services(stringparams, ext_rs_methods) + time_logger.log("runestone placed") # support publisher file, and subtree argument if pub_file: @@ -4752,18 +4779,19 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi # place managed directories - some of these (Asymptote HTML) are # consulted during the XSL run and so need to be placed beforehand copy_managed_directories(tmp_dir, external_abs=external_abs, generated_abs=generated_abs, data_abs=data_dir) + time_logger.log("managed directories copied") if include_static_files: # Copy js and css, but only if not building portable html # place JS in scratch directory copy_html_js(tmp_dir) - - # build or copy theme build_or_copy_theme(xml, pub_vars, tmp_dir) + time_logger.log("css/js copied") # Write output into temporary directory log.info("converting {} to HTML in {}".format(xml, tmp_dir)) xsltproc(extraction_xslt, xml, None, tmp_dir, stringparams) + time_logger.log("xsltproc complete") if not(include_static_files): # remove latex-image generated directories for portable builds @@ -4799,6 +4827,43 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi else: raise ValueError("PTX:BUG: HTML file format not recognized") + time_logger.log("build completed") + + +def html_incremental(xml, pub_file, stringparams, xmlid_root, extra_xsl, dest_dir): + """Update an HTML incrementally in place. + Depends on _static and generated files already being in the destination directory. + Caller must supply: + * stringparams supplemented with: + * rs-js, rs-css, and rs-version (can use _set_runestone_stringparams to set) + * publisher: path to publisher file for use by xsltproc + """ + if not "rs-js" in stringparams: + log.error("Incremental build missing needed stringparam(s). Unable to complete build.") + return False + + # to ensure provided stringparams aren't mutated unintentionally + stringparams = stringparams.copy() + + log_time_info = stringparams.get("profile-py", False) == "yes" + time_logger = Stopwatch("html_incremental()", log_time_info) + + # support publisher file, and subtree argument + if pub_file: + stringparams["publisher"] = pub_file + if xmlid_root: + stringparams["subtree"] = xmlid_root + + # Optional extra XSL could be None, or sanitized full filename + if extra_xsl: + extraction_xslt = extra_xsl + else: + extraction_xslt = os.path.join(get_ptx_xsl_path(), "pretext-html.xsl") + + log.info("incremental convertsion of {} to HTML in {}".format(xml, dest_dir)) + xsltproc(extraction_xslt, xml, None, dest_dir, stringparams) + time_logger.log("xsltproc complete") + def revealjs( xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_file, dest_dir @@ -6113,6 +6178,29 @@ def place_latex_package_files(dest_dir, journal_name, cache_dir): shutil.copy2(file_path, dest_dir) +class Stopwatch: + """A simple stopwatch class for measuring elapsed time. """ + """print_log set to false disables logging of elapsed time """ + + def __init__(self, name:str="", print_log:bool=True): + self.name = name + self.print_log = print_log + self.start_time = time.time() + self.last_log_time = self.start_time + + def reset(self): + """Reset the log timer to the current time.""" + self.last_log_time = time.time() + + def log(self, timepoint_description:str=""): + """Print a log message with the elapsed time since the last log event.""" + if self.print_log: + cur_time = time.time() + elapsed_time = cur_time - self.start_time + since_last_log_time = cur_time - self.last_log_time + self.reset() + log.info(f"** Timing report from {self.name}: {timepoint_description}, {since_last_log_time:.2f}s since last watch reset. {elapsed_time:.2f}s total elapsed time.") + ########################### # diff --git a/pretext/pretext b/pretext/pretext index 0ad04e4cca..55335a690a 100755 --- a/pretext/pretext +++ b/pretext/pretext @@ -782,6 +782,37 @@ def main(): dest_dir, None ) + elif args.format == "html-incremental": + # ----------------------------------------- + # Setup - this work could be done one time in a frontend that is monitoring changes + # Force incremental build flag + stringparams["html.build-incremental"] = "yes" + + log_time_info = stringparams.get("profile-py", False) == "yes" + time_logger = ptx.Stopwatch("pretext:html-incremental", log_time_info) + + # attempt to reuse RS services + if "debug.rs.dev" in stringparams: + rs_js, rs_css, rs_cdn_url, rs_version, services_xml = ptx._runestone_debug_service_info() + else: + rs_js, rs_css, rs_cdn_url, rs_version = ptx.query_existing_runestone_services( + dest_dir=dest_dir, + stringparams=stringparams + ) + ptx._set_runestone_stringparams(stringparams, rs_js, rs_css, rs_version) + time_logger.log("runestone stringparams set") + + # ----------------------------------------- + # Actual incremental build, this is the only work done on each change + ptx.html_incremental( + xml=xml_source, + pub_file=publication_file, + stringparams=stringparams, + xmlid_root=args.xmlid, + extra_xsl=extra_stylesheet, + dest_dir=dest_dir, + ) + time_logger.log("complete incremental build") elif args.format == "html-zip": # no "subtree root" build is possible ptx.html( diff --git a/xsl/pretext-assembly.xsl b/xsl/pretext-assembly.xsl index bb2f9d1583..46d8422d17 100644 --- a/xsl/pretext-assembly.xsl +++ b/xsl/pretext-assembly.xsl @@ -348,6 +348,65 @@ along with PreTeXt. If not, see . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -414,12 +473,43 @@ along with PreTeXt. If not, see . + + + + + + + + + + + + + + + + + + + + + + + + + + + + PTX:INFO: Pruned build tree to out of original nodes. + + + - + - + diff --git a/xsl/pretext-html.xsl b/xsl/pretext-html.xsl index 5f0e1ae67f..ba32e47634 100644 --- a/xsl/pretext-html.xsl +++ b/xsl/pretext-html.xsl @@ -102,7 +102,22 @@ along with MathBook XML. If not, see . - + + + + + + + + + + + + + + + + @@ -291,15 +306,27 @@ along with MathBook XML. If not, see . - - - + + + + + + + + + + + + + + + - + @@ -11149,6 +11176,9 @@ along with MathBook XML. If not, see .
+ +
Preview build. Links and knowls that cross pages may not function correctly.
+