Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
6 changes: 4 additions & 2 deletions doc/guide/author/topics.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3594,7 +3594,7 @@
<subsection xml:id="interactive-program-codelens">
<title>Interactive Programs, CodeLens</title>

<p><term>CodeLens</term> is an interactive version of a computer program, which can be visualized by stepping through the code one statement at a time, watching output, variables, and other data structures change. So it is similar to a debugger, except the reader does not set breakpoints or modify program data on-the-fly. This is possible automatically for several different languages when your <init>HTML</init> is hosted on a Runestone server (<xref ref="runestone"/>). This may also be accomplished <q>in browser</q> when hosted on any old generic web server. The catch is that for a generic server a publisher must generate <term>trace data</term> in advance, typically with the PreTeXt-CLI (<xref ref="processing-CLI"/>). Place the <tag>interactive</tag> attribute on a <tag>program</tag> element with the value <c>codelens</c> to elect this behavior (<c>no</c> is the default value). Also, be sure to specify a language from the supported languages: Python, Java, C, and C++. Consult <xref ref="table-program-interactive"/> below for a summary of various combinations. When an output format does not support an interactive CodeLens instance, the fallback is a static program listing. The <tag>program</tag> should have an <attr>xml:id</attr> that will be used as a unique identifier for the generated trace file needed by the codelens.</p>
<p><term>CodeLens</term> is an interactive version of a computer program, which can be visualized by stepping through the code one statement at a time, watching output, variables, and other data structures change. So it is similar to a debugger, except the reader does not set breakpoints or modify program data on-the-fly. This is possible automatically for several different languages when your <init>HTML</init> is hosted on a Runestone server (<xref ref="runestone"/>). This may also be accomplished <q>in browser</q> when hosted on any old generic web server. The catch is that for a generic server a publisher must generate <term>trace data</term> in advance, typically with the PreTeXt-CLI (<xref ref="processing-CLI"/>). Place the <tag>interactive</tag> attribute on a <tag>program</tag> element with the value <c>codelens</c> to elect this behavior (<c>no</c> is the default value). Also, be sure to specify a language from the supported languages: Python, Java, C, and C++. Consult <xref ref="table-program-interactive"/> below for a summary of various combinations. When an output format does not support an interactive CodeLens instance, the fallback is a static program listing, possibly with a link to the interactive version (see <xref ref="interactive-program-activecode"/> for how to configure the links). The <tag>program</tag> should have an <attr>xml:id</attr> that will be used as a unique identifier for the generated trace file needed by the codelens.</p>

<p>Sometimes, there is uninteresting setup to get through before the portion of the program the author wants to illustrate. The author can use <attr>starting-step</attr> to specify the instruction number that the interactive should start at. Code before that will be fast-forwarded through and execution will begin at the indicted step. Note that <attr>starting-step</attr> is a <q>Step</q> number in the execution sequence, not a line number in the source code. You may have to run the Codelens once to find the right step to start at.</p>

Expand All @@ -3610,7 +3610,9 @@

<p><term>ActiveCode</term> is an interactive environment where a reader may work on code through repeated edit-compile-test cycles. Code can be provided by an author as a complete program to be modified, a partial program to be completed, or nothing at all. One good example is that maybe header files, import statement, and similar are provided, and a skeleton of a main entry-point procedure is also provided. Then a reader can concentrate on the more conceptual parts of the programming. Some languages will be executable <q>in browser</q> on any old generic web server, while others must be on a Runestone server (<xref ref="runestone"/>) where a <url href="https://github.com/trampgeek/jobe" visual="github.com/trampgeek/jobe">Jobe Server</url> is running to support the execution.</p>

<p>Place the <attr>interactive</attr> attribute on a <tag>program</tag> element with the value <c>activecode</c> to elect this behavior (<c>no</c> is the default value). Also, be sure to specify a language from the supported languages. Consult <xref ref="table-program-interactive"/> below for a summary of various combinations. When an output format does not support an interactive ActiveCode instance, the fallback is a static program listing.</p>
<p>Place the <attr>interactive</attr> attribute on a <tag>program</tag> element with the value <c>activecode</c> to elect this behavior (<c>no</c> is the default value). Also, be sure to specify a language from the supported languages. Consult <xref ref="table-program-interactive"/> below for a summary of various combinations.</p>

<p>When an output format does not support an interactive ActiveCode instance, the fallback is a static program listing. The static listing is possibly supplemented with a link and QR code to access an HTML version. To get these links, you must specify a base <init>URL</init> (see <xref ref="online-baseurl"/>). If you have a base <init>URL</init> and want to disable the QR codes, you can set the <attr>static-qrcodes</attr> attribute to no <c>"no"</c> in the publisher variable <c>common/program</c>.</p>

<warning>
<title>Labels and XML IDs</title>
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 @@ -4397,6 +4399,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 @@ -4442,15 +4450,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 @@ -4477,11 +4478,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 @@ -4586,6 +4593,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 @@ -4859,6 +4881,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 @@ -4869,6 +4894,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 @@ -4878,6 +4904,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 @@ -4896,18 +4923,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 @@ -4943,6 +4971,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 @@ -6262,6 +6327,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 @@ -783,6 +783,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
3 changes: 3 additions & 0 deletions xsl/entities.ent
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,6 @@
<!-- are added, then this need not change -->
<!-- Typical use: self::exercise and boolean(&INLINE-EXERCISE-FILTER;) -->
<!ENTITY INLINE-EXERCISE-FILTER "parent::article|parent::paragraphs|parent::chapter|parent::section|parent::subsection|parent::subsubsection|parent::handout">

<!-- interactive elements that need a QR code generated -->
<!ENTITY QRCODE-INTERACTIVES "audio[@source|@href]|video[@source|@href|@youtube|@youtubeplaylist|@vimeo]|interactive|program[@interactive != '' and @interactive != 'no']">
Loading