diff --git a/.dockerignore b/.dockerignore index e3105b9..ad3294a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,5 @@ ! requirements*.txt ! asl_rulebook2/ ! docker/ +! doc/ ! LICENSE.txt diff --git a/Dockerfile b/Dockerfile index 7100cee..ffefadc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,7 @@ RUN if [ -n "$CONTROL_TESTS_PORT" ]; then \ # install the application WORKDIR /app COPY asl_rulebook2/ ./asl_rulebook2/ +COPY doc/ ./doc/ COPY setup.py requirements.txt requirements-dev.txt LICENSE.txt ./ RUN pip3 install --editable . diff --git a/asl_rulebook2/webapp/__init__.py b/asl_rulebook2/webapp/__init__.py index 53b081f..f8acb1e 100644 --- a/asl_rulebook2/webapp/__init__.py +++ b/asl_rulebook2/webapp/__init__.py @@ -84,6 +84,7 @@ import asl_rulebook2.webapp.content #pylint: disable=wrong-import-position,cycli import asl_rulebook2.webapp.search #pylint: disable=wrong-import-position,cyclic-import import asl_rulebook2.webapp.rule_info #pylint: disable=wrong-import-position,cyclic-import import asl_rulebook2.webapp.prepare #pylint: disable=wrong-import-position,cyclic-import +import asl_rulebook2.webapp.doc #pylint: disable=wrong-import-position,cyclic-import from asl_rulebook2.webapp import globvars #pylint: disable=wrong-import-position,cyclic-import app.before_request( globvars.on_request ) diff --git a/asl_rulebook2/webapp/doc.py b/asl_rulebook2/webapp/doc.py new file mode 100644 index 0000000..6163fd5 --- /dev/null +++ b/asl_rulebook2/webapp/doc.py @@ -0,0 +1,39 @@ +""" Serve documentation files. """ + +import os +import io +import re + +import markdown +from flask import make_response, send_file, abort, safe_join + +from asl_rulebook2.webapp import app + +# --------------------------------------------------------------------- + +@app.route( "/doc/" ) +def get_doc( path ): + """Return the specified documentation file.""" + + # locate the documentation file + doc_dir = os.path.join( os.path.dirname( __file__ ), "../../doc/" ) + fname = safe_join( doc_dir, path ) + if not os.path.isfile( fname ): + abort( 404 ) + + # check if the file is Markdown + if os.path.splitext( path )[1] == ".md": + # yup - convert it to HTML + buf = io.BytesIO() + markdown.markdownFromFile( input=fname, output=buf, encoding="utf-8" ) + # FUDGE! Code fragments are wrapped with tags, and while we would like to style them using CSS, + # there's no way to distinguish between inline and block fragments :-/ We identify block fragments + # by the tag being at the start of a line, and style them using inline attributes. Sigh... + code = b"" + resp = re.sub( rb"^", code, buf.getvalue(), flags=re.MULTILINE ) + resp = make_response( resp ) + resp.mimetype = "text/html" + return resp + else: + # nope - just serve it as-is + return send_file( fname ) diff --git a/asl_rulebook2/webapp/static/css/prepare.css b/asl_rulebook2/webapp/static/css/prepare.css index 7deea7a..9009528 100644 --- a/asl_rulebook2/webapp/static/css/prepare.css +++ b/asl_rulebook2/webapp/static/css/prepare.css @@ -19,15 +19,22 @@ code { display: block ; margin: 5px 0 5px 20px ; } #progress-panel { flex-grow: 1 ; overflow-y: auto ; border: 1px solid black ; border-radius: 5px ; padding: 10px ; - font-family: monospace ; font-size: 90% ; } #progress-panel .progress { font-style: italic ; } -#progress-panel .status { margin: 5px 0 ; } +#progress-panel .status { margin: 5px 0 ; font-family: monospace ; font-size: 90% ; } #progress-panel .status:first-of-type { margin-top: 0 ; } #progress-panel .status table { margin-left: 2px ; } #progress-panel .status table td { vertical-align: top ; } #progress-panel .status img.icon { height: 15px ; margin: 1px 3px 0 0 ; } +#progress-panel .loading { + position: fixed ; bottom: 18px ; right: 18px ; + border: 1px solid black ; border-radius: 5px ; + padding: 10px ; + background: #f0f0f0 ; + font-size: 80% ; font-style: italic ; text-align: center ; +} + #download-panel { position: fixed ; bottom: 18px ; right: 18px ; width: 75% ; border: 1px solid black ; border-radius: 5px ; background: white ; diff --git a/asl_rulebook2/webapp/static/prepare.js b/asl_rulebook2/webapp/static/prepare.js index 0e536be..3f2938d 100644 --- a/asl_rulebook2/webapp/static/prepare.js +++ b/asl_rulebook2/webapp/static/prepare.js @@ -31,7 +31,7 @@ gPrepareApp.component( "prepare-app", { It will take around 10-15 minutes.

If there are problems, you can try to prepare your data files manually, - as described here. + as described here.

@@ -185,11 +185,18 @@ gPrepareApp.component( "progress-panel", { data() { return { socketIOClient: null, statusBlocks: [], + isDone: false, } ; }, template: `
+
+ +
+ While you're waiting, you can
check out the features here. +
+
`, created() { @@ -202,10 +209,9 @@ gPrepareApp.component( "progress-panel", { initSocketIOClient() { // initialize the socketio client - let done = false ; this.socketIOClient = io.connect() ; //eslint-disable-line no-undef this.socketIOClient.on( "disconnect", () => { - if ( ! done ) + if ( ! this.isDone ) this.$emit( "fatal", "The server has gone away. Please restart it, then reload this page." ) ; } ) ; this.socketIOClient.on( "status", (msg) => { this.addStatusBlock( msg ) ; } ) ; @@ -213,7 +219,7 @@ gPrepareApp.component( "progress-panel", { this.socketIOClient.on( "warning", (msg) => { this.addProgressMsg( "warning", msg ) ; } ) ; this.socketIOClient.on( "error", (msg) => { this.addProgressMsg( "error", msg ) ; } ) ; this.socketIOClient.on( "done", (downloadUrl) => { - done = true ; + this.isDone = true ; gProgressPanel.addStatusBlock( "All done." ) ; this.socketIOClient.disconnect() ; this.socketIOClient = null ; diff --git a/asl_rulebook2/webapp/tests/test_doc.py b/asl_rulebook2/webapp/tests/test_doc.py new file mode 100644 index 0000000..c78efd5 --- /dev/null +++ b/asl_rulebook2/webapp/tests/test_doc.py @@ -0,0 +1,35 @@ +""" Test documentation files. """ + +import urllib.request +import urllib.error + +import pytest + +from asl_rulebook2.webapp.tests.utils import init_webapp + +# --------------------------------------------------------------------- + +def test_doc( webapp, webdriver ): + """Test serving documentation files.""" + + # initialize + webapp.control_tests.set_data_dir( "simple" ) + init_webapp( webapp, webdriver ) + + def get_doc( path ): + # get the specified documentation file + url = "{}/{}".format( webapp.base_url, path ) + resp = urllib.request.urlopen( url ).read() + return resp.decode( "utf-8" ) + + # test a valid documentation file + resp = get_doc( "/doc/prepare.md" ) + assert "Preparing the data files" in resp + + # test an unknown documentation file + with pytest.raises( urllib.error.HTTPError ): + _ = get_doc( "/doc/UNKNOWN" ) + + # try to bust out of the documentation directory + with pytest.raises( urllib.error.HTTPError ): + _ = get_doc( "/doc/../LICENSE.txt" ) diff --git a/doc/extend.md b/doc/extend.md index ee739c1..70beee1 100644 --- a/doc/extend.md +++ b/doc/extend.md @@ -19,6 +19,7 @@ Finally, bookmarks need to be created in the PDF for each rule, so that the prog Save the prepared file as `ASL Rulebook (xxx).pdf` in your data directory, together with the targets file, then restart the server. Optionally, you can also provide: + - a `ASL Rulebook (xxx).chapters` file (to be able to browse the PDF in the *Chapters* panel) - a `ASL Rulebook (xxx).footnotes` file (if the rules have any footnotes). @@ -29,6 +30,7 @@ To add a chapter icon and background, create files `XXX-icon.png` and `XXX-backg To add rules for a module not already referenced by the ASLRB index, the process is the same as above, but you also need to write a `.index` file. As an example, take a look at the `ASL Rulebook.index` file that was extracted for you. All the files should have the same base filename e.g. + - kgs.index - kgs.pdf - kgs.targets @@ -45,6 +47,7 @@ This is described [here](../asl_rulebook2/webapp/tests/fixtures/full/). As you add more content, program startup will use more and more CPU (as it converts rule ID's to clickable links), and while the program will still come up and be functional quickly, rule ID's will take longer to become clickable, and this processing may affect other things running on your computer. To alleviate this, you can specify a file to cache the results of this work: + - add a `CACHED_SEARCHDB` settings to your `site.cfg` file (if running from source) - add a `--cached-searchdb` parameter when running `run-container.sh` (if running using Docker) diff --git a/requirements.txt b/requirements.txt index 76f28c2..705f3df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ flask-socketio==5.1.1 eventlet==0.33.0 pyyaml==5.4.1 lxml==4.6.2 +markdown==3.3.6 click==7.1.2 pdfminer.six==20201018