📃 Make it easier to serve single markdown files

This commit is contained in:
Jérôme Petazzoni
2025-12-03 17:47:15 -06:00
parent f9d73c0a1e
commit e8e2123457
4 changed files with 88 additions and 59 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ slides/index.html
slides/past.html
slides/slides.zip
slides/_academy_*
slides/fragments
node_modules
### macOS ###

View File

@@ -23,3 +23,10 @@
# Survey form
/please https://docs.google.com/forms/d/e/1FAIpQLSfIYSgrV7tpfBNm1hOaprjnBHgWKn5n-k5vtNXYJkOX1sRxng/viewform
# Serve individual lessons with special URLs:
# - access http://container.training/k8s/ingress
# - ...redirects to http://container.training/view?k8s/ingress
# - ...proxies to http://container.training/workshop.html?k8s/ingress
/view /workshop.html 200
/* /view?:splat

View File

@@ -87,26 +87,6 @@ def flatten(titles):
def generatefromyaml(manifest, filename):
manifest = yaml.safe_load(manifest)
for k in manifest:
override = os.environ.get("OVERRIDE_"+k)
if override:
manifest[k] = override
for k in ["chat", "gitrepo", "slides", "title"]:
if k not in manifest:
manifest[k] = ""
if "zip" not in manifest:
if manifest["slides"].endswith('/'):
manifest["zip"] = manifest["slides"] + "slides.zip"
else:
manifest["zip"] = manifest["slides"] + "/slides.zip"
if "html" not in manifest:
manifest["html"] = filename + ".html"
markdown, titles = processcontent(manifest["content"], filename)
logging.debug("Found {} titles.".format(len(titles)))
toc = gentoc(titles)
@@ -121,39 +101,39 @@ def generatefromyaml(manifest, filename):
exclude = ",".join('"{}"'.format(c) for c in exclude)
# Insert build info. This is super hackish.
markdown = markdown.replace(
".debug[",
".debug[\n```\n{}\n```\n\nThese slides have been built from commit: {}\n\n".format(dirtyfiles, commit),
1)
markdown = markdown.replace("@@TITLE@@", manifest["title"].replace("\n", "<br/>"))
html = open("workshop.html").read()
html = html.replace("@@TITLE@@", manifest["title"].replace("\n", " "))
html = html.replace("@@MARKDOWN@@", markdown)
html = html.replace("@@EXCLUDE@@", exclude)
html = html.replace("@@CHAT@@", manifest["chat"])
html = html.replace("@@GITREPO@@", manifest["gitrepo"])
html = html.replace("@@SLIDES@@", manifest["slides"])
html = html.replace("@@ZIP@@", manifest["zip"])
html = html.replace("@@HTML@@", manifest["html"])
html = html.replace("@@TITLE@@", manifest["title"].replace("\n", " "))
html = html.replace("@@SLIDENUMBERPREFIX@@", manifest.get("slidenumberprefix", ""))
return html
def processAtAtStrings(text):
text = text.replace("@@CHAT@@", manifest["chat"])
text = text.replace("@@GITREPO@@", manifest["gitrepo"])
text = text.replace("@@SLIDES@@", manifest["slides"])
text = text.replace("@@ZIP@@", manifest["zip"])
text = text.replace("@@HTML@@", manifest["html"])
text = text.replace("@@TITLE@@", manifest["title"].replace("\n", "<br/>"))
# Process @@LINK[file] and @@INCLUDE[file] directives
local_anchor_path = ".."
# FIXME use dynamic repo and branch?
online_anchor_path = "https://github.com/jpetazzo/container.training/tree/master"
for atatlink in re.findall(r"@@LINK\[[^]]*\]", html):
online_anchor_path = "https://github.com/jpetazzo/container.training/tree/main"
for atatlink in re.findall(r"@@LINK\[[^]]*\]", text):
logging.debug("Processing {}".format(atatlink))
file_name = atatlink[len("@@LINK["):-1]
html = html.replace(atatlink, "[{}]({}/{})".format(file_name, online_anchor_path, file_name ))
for atatinclude in re.findall(r"@@INCLUDE\[[^]]*\]", html):
text = text.replace(atatlink, "[{}]({}/{})".format(file_name, online_anchor_path, file_name ))
for atatinclude in re.findall(r"@@INCLUDE\[[^]]*\]", text):
logging.debug("Processing {}".format(atatinclude))
file_name = atatinclude[len("@@INCLUDE["):-1]
file_path = os.path.join(local_anchor_path, file_name)
html = html.replace(atatinclude, open(file_path).read())
return html
text = text.replace(atatinclude, open(file_path).read())
return text
# Maps a title (the string just after "^# ") to its position in the TOC
@@ -213,7 +193,14 @@ def processcontent(content, filename):
content += "\n" + slidefooter
return (content, titles)
if os.path.isfile(content):
return processcontent(open(content).read(), content)
markdown = open(content).read()
markdown = processAtAtStrings(markdown)
fragmentfile = os.path.join("fragments", content)
fragmentdir = os.path.dirname(fragmentfile)
os.makedirs(fragmentdir, exist_ok=True)
with open(fragmentfile, "w") as f:
f.write(markdown)
return processcontent(markdown, content)
logging.warning("Content spans only one line (it's probably a file name) but no file found: {}".format(content))
if isinstance(content, list):
subparts = [processcontent(c, filename) for c in content]
@@ -271,5 +258,22 @@ else:
else:
manifest = open(filename)
logging.info("Processing {}...".format(filename))
manifest = yaml.safe_load(manifest)
for k in manifest:
override = os.environ.get("OVERRIDE_"+k)
if override:
manifest[k] = override
for k in ["chat", "gitrepo", "slides", "title"]:
if k not in manifest:
manifest[k] = ""
if "zip" not in manifest:
if manifest["slides"].endswith('/'):
manifest["zip"] = manifest["slides"] + "slides.zip"
else:
manifest["zip"] = manifest["slides"] + "/slides.zip"
if "html" not in manifest:
manifest["html"] = filename + ".html"
sys.stdout.write(generatefromyaml(manifest, filename))
logging.info("Processed {}.".format(filename))

View File

@@ -1,43 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<title>@@TITLE@@</title>
<title>Training Materials</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="workshop.css">
</head>
<body>
<!--
<div style="position: absolute; left: 20%; right: 20%; top: 30%;">
<h1 style="font-size: 3em;">Loading ...</h1>
The slides should show up here. If they don't, it might be
because you are accessing this file directly from your filesystem.
It needs to be served from a web server. You can try this:
<pre>
docker-compose up -d
open http://localhost:8888/workshop.html # on MacOS
xdg-open http://localhost:8888/workshop.html # on Linux
</pre>
Once the slides are loaded, this notice disappears when you
go full screen (e.g. by hitting "f").
</div>
-->
<textarea id="source">@@MARKDOWN@@</textarea>
<script src="remark.min.js" type="text/javascript">
</script>
<script type="text/javascript">
<div id="loading">⏳️ Loading...</div>
<textarea id="source" style="display: none;">@@MARKDOWN@@</textarea>
<script type="module">
if (window.location.search[0] === '?') {
const contentName = window.location.search.substr(1);
let contentText = "⚠️ Failed to load content!"
for (const url of [
`fragments/${contentName}.md`,
`${contentName}.md`
]) {
const contentResponse = await fetch(url);
if (contentResponse.ok) {
contentText = await contentResponse.text();
break;
}
}
document.querySelector('#source').textContent = contentText;
}
/*
This tmpl() function helps us to use the same HTML file for entire
decks (used for live classes and pre-processed by markmaker.py to
replace @@-strings) and to display individual chapters (used for
video recording and not pre-processed by markmarker.py, so we still
have @@-strings in this HTML file in that case).
*/
function tmpl(atString, templateValue, defaultValue) {
if (atString[0] === "@") {
return defaultValue;
}
return JSON.parse(templateValue);
}
var tmplEXCLUDE = tmpl('@@EXCLUDE@@', '[@@EXCLUDE@@]', []);
var tmplTITLE = tmpl('@@TITLE@@', '"@@TITLE@@"', document.title);
var tmplSLIDENUMBERPREFIX = tmpl('@@SLIDENUMBERPREFIX@@', '"@@SLIDENUMBERPREFIX@@"', "");
document.title = tmplTITLE;
var slideshow = remark.create({
ratio: '16:9',
highlightSpans: true,
slideNumberFormat: '@@SLIDENUMBERPREFIX@@%current%/%total%',
excludedClasses: [@@EXCLUDE@@]
slideNumberFormat: `${tmplSLIDENUMBERPREFIX}%current%/%total%`,
excludedClasses: tmplEXCLUDE
});
</script>
<script type="module">
document.querySelector('#loading').style.display = 'none';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false });
slideshow.on('afterShowSlide', function (slide) {
mermaid.run({
nodes: document.querySelectorAll('div.remark-visible .mermaid'),
nodes: document.querySelectorAll('div.remark-visible.mermaid'),
});
});
// Reminder, if you want to tinker with mermaid,