Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions css/components/elements/_misc-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down
116 changes: 102 additions & 14 deletions pretext/lib/pretext.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
# contextmanager tools
import contextlib

import time

# cleanup multiline strings used as source code
import textwrap

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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"""
Expand Down Expand Up @@ -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'''

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.")


###########################
#
Expand Down
31 changes: 31 additions & 0 deletions pretext/pretext
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
94 changes: 92 additions & 2 deletions xsl/pretext-assembly.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,65 @@ along with PreTeXt. If not, see <http://www.gnu.org/licenses/>.
</xsl:copy>
</xsl:template>

<!-- Pruning templates generate a subtree towards $target-node -->
<!-- which prunes as many other branches as possible while -->
<!-- retaining reasonably decent output of the desire tree. -->
<!-- i.e. drop content other than titled structural elements. -->
<xsl:template match="@*" mode="pruning">
<xsl:apply-templates select="." mode="pruning-keep"/>
</xsl:template>

<xsl:template match="node()" mode="pruning">
<xsl:param name="target-node"/>
<xsl:choose>
<!-- This node contains the target node, copy and keep going -->
<xsl:when test="count(descendant::*|$target-node) = count(descendant::*)">
<xsl:copy>
<xsl:apply-templates select="node()|@*" mode="pruning">
<xsl:with-param name="target-node" select="$target-node"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:when>
<!-- This node is target node, keep it all -->
<xsl:when test="count(.|$target-node) = 1">
<xsl:apply-templates select="." mode="pruning-keep"/>
</xsl:when>
<!-- Shift to aggressive pruning -->
<xsl:otherwise>
<xsl:apply-templates select="." mode="pruning-prune"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<!-- Certain structural elements have data/metadata that we want to -->
<!-- preserve, so we copy them over entirely. -->
<xsl:template match="frontmatter|docinfo|bookinfo|backmatter" mode="pruning">
<xsl:copy>
<xsl:apply-templates select="node()|@*" mode="pruning-keep"/>
</xsl:copy>
</xsl:template>

<!-- Preserve anything from here on down -->
<xsl:template match="node()|@*" mode="pruning-keep">
<xsl:copy>
<xsl:apply-templates select="node()|@*" mode="pruning-keep"/>
</xsl:copy>
</xsl:template>

<!-- Once aggressively pruning, only keep structural elements that give us -->
<!-- numbering and toc info. -->
<xsl:template match="&STRUCTURAL;|@*" mode="pruning-prune">
<xsl:copy>
<xsl:apply-templates select="&STRUCTURAL;|title|@*" mode="pruning-prune">
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

<!-- If we hit a title, grab all the contents so the ToC gets content-->
<xsl:template match="title" mode="pruning-prune">
<xsl:apply-templates select="." mode="pruning-keep"/>
</xsl:template>

<!-- These templates initiate and create several iterations of -->
<!-- the source tree via modal templates. Think of each as a -->
<!-- "pass" through the source. Generally this constructs the -->
Expand Down Expand Up @@ -414,12 +473,43 @@ along with PreTeXt. If not, see <http://www.gnu.org/licenses/>.
</xsl:variable>
<xsl:variable name="original-labeled" select="exsl:node-set($original-labeled-rtf)"/>

<!-- See pretext-html for full version of these params -->
<!-- "forward declared" here to avoid stylesheet ordering conflict -->
<xsl:param name="subtree" select="''"/>
<xsl:param name="html.build-preview" select="''"/>
<!-- After labeling, but before further processing, build a -->
<!-- pruned tree. Most of the time it is a copy of the labeled tree -->
<!-- but when doing a preview build of a subtree, we prune -->
<!-- down the labeled tree to the desired subtree and select other -->
<!-- important elements. -->
<xsl:variable name="pruning-tree-rtf">
<xsl:choose>
<xsl:when test="$subtree != '' and $html.build-preview = 'yes'">
<xsl:variable name="target-node" select="$original-labeled//*[@xml:id = $subtree]"/>
<xsl:apply-templates select="$original-labeled" mode="pruning">
<xsl:with-param name="target-node" select="$original-labeled//*[@xml:id = $subtree]"/>
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$original-labeled"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="pruning-tree" select="exsl:node-set($pruning-tree-rtf)"/>

<!-- Variable just to allow some printing after pruning-tree is established -->
<xsl:variable name="not-used-pruning-info">
<xsl:if test="$subtree != '' and $html.build-preview = 'yes'">
<xsl:message>PTX:INFO: Pruned build tree to <xsl:value-of select="count($pruning-tree//node())"/> out of original <xsl:value-of select="count($original-labeled//node())"/> nodes.</xsl:message>
</xsl:if>
</xsl:variable>

<!-- A global list of all "webwork" used for -->
<!-- efficient backward-compatible indentification -->
<xsl:variable name="all-webwork" select="$original-labeled//webwork"/>
<xsl:variable name="all-webwork" select="$pruning-tree//webwork"/>

<xsl:variable name="webwork-rtf">
<xsl:apply-templates select="$original-labeled" mode="webwork"/>
<xsl:apply-templates select="$pruning-tree" mode="webwork"/>
</xsl:variable>
<xsl:variable name="webworked" select="exsl:node-set($webwork-rtf)"/>

Expand Down
Loading