diff --git a/META-INF/RASCAL.MF b/META-INF/RASCAL.MF
index d3977080a63..a92afc8e206 100644
--- a/META-INF/RASCAL.MF
+++ b/META-INF/RASCAL.MF
@@ -1,5 +1,5 @@
Project-Name: rascal
-Source: src/org/rascalmpl/library,test/org/rascalmpl/benchmark,test//org/rascalmpl/test/data
+Source: src/org/rascalmpl/library,src/org/rascalmpl/tutor,test/org/rascalmpl/benchmark,test/org/rascalmpl/test/data
Courses: src/org/rascalmpl/courses
diff --git a/pom.xml b/pom.xml
index 2781f02313a..2ed31386d1d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
scm:git:ssh://git@github.com/usethesource/rascal.git
- v0.41.0-RC1
+ HEAD
@@ -137,6 +137,7 @@
${project.build.outputDirectory}
${project.basedir}/src/org/rascalmpl/library
+ ${project.basedir}/src/org/rascalmpl/tutor
|std:///|
${project.basedir}/FUNDING
diff --git a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java
index 875327dde1c..c05f62add04 100644
--- a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java
+++ b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java
@@ -180,6 +180,11 @@ public void cancelRunningCommandRequested() {
eval.endAllJobs();
}
+ public void cleanEnvironment() {
+ Objects.requireNonNull(eval, "Not initialized yet");
+ eval.getCurrentModuleEnvironment().reset();
+ }
+
@Override
public ICommandOutput stackTraceRequested() {
Objects.requireNonNull(eval, "Not initialized yet");
diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/Compiler.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/Compiler.rsc
new file mode 100644
index 00000000000..3131a42534f
--- /dev/null
+++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/Compiler.rsc
@@ -0,0 +1,977 @@
+@bootstrapParser
+@synopsis{compiles .rsc and .md files to markdown by executing Rascal-specific code and inlining its output}
+@description{
+ This compiler collects .rsc files and .md files from a PathConfig's srcs folders.
+
+ Every .rsc file is compiled to a .md file with an outline of the declarations contained
+ in the file and the contents of the @synopsis, @description, @pitfalls, @benefits, @examples
+ tags with those declarations. @doc is also supported for backward compatibility's purposes.
+ The resulting markdown is processed by the rest of the compiler, as if written by hand.
+
+ Every .md file is scanned for rascal-shell between triple backticks elements. The contents between the backticks are
+ executed by a private Rascal REPL and the output is captured in different ways. Normal IO
+ via stderr and stdout is literally printed back and HTML or image output is inlined into
+ the document.
+
+ For (nested) folders in the srcs folders, which do not contain an `index.md` file, or
+ a `.md` file where the name is equal to the name of the current folder, a fresh index.md
+ file is generated.
+}
+module lang::rascal::tutor::Compiler
+
+import Message;
+import Exception;
+import IO;
+import String;
+import Node;
+import List;
+import Relation;
+import Location;
+import ParseTree;
+import util::Reflective;
+import util::FileSystem;
+import ValueIO;
+
+import lang::yaml::Model;
+import lang::rascal::tutor::repl::TutorCommandExecutor;
+import lang::rascal::tutor::apidoc::GenerateMarkdown;
+import lang::rascal::tutor::apidoc::ExtractInfo;
+import lang::rascal::tutor::Indexer;
+import lang::rascal::tutor::Names;
+import lang::rascal::tutor::Output;
+import lang::rascal::tutor::Includer;
+import lang::rascal::\syntax::Rascal;
+
+public PathConfig defaultConfig
+ = pathConfig(
+ bin=|target://rascal-tutor/docs|,
+ libs=[|lib://rascal|],
+ srcs=[
+ |project://rascal-tutor/src/lang/rascal/tutor/examples/Test|
+ ]);
+
+public list[Message] lastErrors = [];
+
+public void defaultCompile(bool clean=false) {
+ if (clean) {
+ remove(defaultConfig.bin, recursive=true);
+ }
+ errors = compile(defaultConfig);
+
+ for (e <- errors) {
+ println(":
+ ' <}>");
+ }
+
+ lastErrors = errors;
+}
+
+@synopsis{compiles each pcfg.srcs folder as a course root}
+list[Message] compile(PathConfig pcfg, CommandExecutor exec = createExecutor(pcfg)) {
+ ind = createConceptIndex(pcfg);
+
+ if (pcfg.isPackageCourse) {
+ generatePackageIndex(pcfg);
+ }
+ else {
+ storeImportantProjectMetaData(pcfg);
+ }
+
+ // remove trailing slashes
+ pcfg.ignores = [i.parent + i.file | i <- pcfg.ignores];
+
+ return [*compileCourse(src, pcfg[currentRoot=src], exec, ind) | src <- pcfg.srcs];
+}
+
+void storeImportantProjectMetaData(PathConfig pcfg) {
+ // these files are with the .txt extension such that they are not automatically
+ // incorporated into the website. Rather other pages can include them where they see fit.
+ // this information, however, is not easy to obtain outside of the build
+ // environment of the current project. Therefore we store it here and now.
+
+ if (!pcfg.packageName?) {
+ return;
+ }
+
+ if (pcfg.license? && exists(pcfg.license)) {
+ copy(pcfg.license, pcfg.bin + "LICENSE_.txt");
+ }
+
+ if (pcfg.citation? && exists(pcfg.citation)) {
+ copy(pcfg.citation, pcfg.bin + "CITATION_.md");
+ }
+
+ if (pcfg.funding? && exists(pcfg.funding)) {
+ copy(pcfg.funding, pcfg.bin + "FUNDING_.md");
+ }
+
+ if (pcfg.releaseNotes? && exists(pcfg.releaseNotes)) {
+ copy(pcfg.releaseNotes, pcfg.bin + "RELEASE-NOTES_.md");
+ }
+
+ dependencies = [ f | f <- pcfg.classloaders, exists(f), f.extension=="jar"];
+
+ if (dependencies != []) {
+ writeFile(pcfg.bin + "DEPENDENCIES_.txt",
+ " *
+ '<}>
+ "
+ );
+ }
+}
+
+void generatePackageIndex(PathConfig pcfg) {
+ targetFile = pcfg.bin + "Packages" + package(pcfg.packageName) + "index.md";
+
+ if (pcfg.license?) {
+ writeFile(targetFile.parent + "License.md",
+ "---
+ 'title: License
+ '---
+ '
+ '");
+ }
+
+ if (pcfg.funding?) {
+ writeFile(targetFile.parent + "Funding.md",
+ "---
+ 'title: Funding
+ '---
+ '
+ ':::info
+ 'Open-source software is free for use, yet it does not come for free.
+ 'The following sources of funding have been instrumental in the creation
+ 'and maintenance of . You may consider also to become
+ 'a [sponsor](https://github.com/sponsors/usethesource?o=esb)
+ ':::
+ '
+ '");
+ }
+
+ if (pcfg.citation?) {
+ writeFile(targetFile.parent + "Citation.md",
+ "---
+ 'title: Citation
+ '---
+ '
+ ':::info
+ 'Open-source software is [citeable](https://www.software.ac.uk/how-cite-software) output of research and development efforts.
+ 'Citing software **recognizes** the associated investment and the quality of the result.
+ 'If you use open-source software, it is becoming standard practise to recognize the work as
+ 'its authors have indicated below. In turn their effort might be **awarded** with renewed [funding](../../Packages//Funding.md)<} else {>funding<}> for
+ 'based on the evidence of your appreciation, and it may help their individual career perspectives.
+ ':::
+ '
+ '");
+ }
+
+ if (pcfg.releaseNotes?) {
+ writeFile(targetFile.parent + "RELEASE-NOTES.md",
+ "---
+ 'title: Release notes
+ '---
+ '
+ '");
+ }
+
+ dependencies = [ f | f <- pcfg.classloaders, exists(f), f.extension=="jar"];
+
+ if (dependencies != []) {
+ writeFile(targetFile.parent + "Dependencies.md",
+ "---
+ 'title: Dependencies
+ '---
+ '
+ 'These are compile-time and run-time dependencies of :
+ '
+ ' *
+ '<}>
+ '
+ ':::info
+ 'You should check that the licenses of the above dependencies are compatible with your goals and situation. The authors and owners of cannot be held liable for any damages caused by the use of those licenses, or changes therein.
+ '
+ 'The authors contributing to do prefer open-source licenses for their dependencies that are permissive to commercial exploitation and any kind of reuse, and that are non-viral.
+ ':::
+ "
+ );
+ }
+
+ writeFile(targetFile.parent + "index.md",
+ "---
+ 'title:
+ '---
+ '
+ 'This is the documentation for version of .
+ '
+ '* [API documentation](../../Packages//API)<}>
+ '* [](../../Packages//)
+ '<}>* [Stackoverflow questions](https://stackoverflow.com/questions/tagged/rascal+)
+ '* [Release notes](../../Packages//RELEASE-NOTES.md)<}>
+ '* [Open-source license](../../Packages//License.md)<}>
+ '* How to [cite this software](../../Packages//Citation.md)<}>
+ '* [Funding sources](../../Packages//Funding.md) sources.<}>
+ '* [Dependencies](../../Packages//Dependencies.md)<}>
+ '* [Source code](<""[1..-1]>)<}>
+ '* [Issue tracker](<""[1..-1]>)<}>
+ '
+ '#### Installation
+ '
+ 'To use in a maven-based Rascal project, include the following dependency in the `pom.xml` file:
+ '
+ '```xml
+ '\
+ ' \
+ ' \\
+ ' \\
+ ' \\
+ ' \
+ '\
+ '```
+ '**and** change the `Require-Libraries` field in `/path/to/yourProjectName/META-INF/RASCAL.MF` like so:
+ '
+ '```MF
+ 'Manifest-Version: 0.0.1
+ 'Project-Name: yourProjectName
+ 'Source: path/to/src
+ 'Require-Libraries: |lib://|
+ '
+ '
+ '```
+ ':::info
+ 'dot.MF files _must_ end with an empty line.
+ ':::
+ ");
+}
+
+list[Message] compileCourse(loc root, PathConfig pcfg, CommandExecutor exec, Index ind)
+ = compileDirectory(root, pcfg[currentRoot=root], exec, ind);
+
+list[Message] compile(loc src, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) {
+ if (src in pcfg.ignores) {
+ return [info("skipped ignored location: ", src)];
+ }
+
+ // new concept, new execution environment:
+ exec.reset();
+
+ if (isDirectory(src), src.file != "internal") {
+ return compileDirectory(src, pcfg, exec, ind, sidebar_position=sidebar_position);
+ }
+ else if (src.extension == "rsc") {
+ return compileRascalFile(src, pcfg[currentFile=src], exec, ind);
+ }
+ else if (src.extension in {"md"}) {
+ return compileMarkdownFile(src, pcfg, exec, ind, sidebar_position=sidebar_position);
+ }
+ else if (src.extension in {"png","jpg","svg","jpeg", "html", "js"}) {
+ try {
+ println("copying [Asset]");
+ copy(src, pcfg.bin + (pcfg.isPackageCourse ? "assets/Packages/" : "assets") + capitalize(pcfg.currentRoot.file) + relativize(pcfg.currentRoot, src).path);
+
+ return [];
+ }
+ catch IO(str message): {
+ return [error(message, src)];
+ }
+ }
+ else {
+ return [];
+ }
+}
+
+list[Message] compileDirectory(loc d, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) {
+ if (d in pcfg.ignores) {
+ return [info("skipped ignored location: ", d)];
+ }
+
+ println("compiling [Folder]");
+
+ indexFiles = {(d + "")[extension="md"], (d + "index.md")};
+
+ if (!exists(d)) {
+ return [warning("Course folder does not exist on disk: ", d)];
+ }
+
+ output = [];
+ errors = [];
+ nestedDtls = [];
+
+ if (i <- indexFiles && exists(i)) {
+ // this can only be a markdown file (see above)
+ j=i;
+ j.file = (j.file == j.parent[extension="md"].file) ? "index.md" : j.file;
+
+ targetFile = pcfg.bin
+ + (pcfg.isPackageCourse ? "Packages/" : "")
+ + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file))
+ + relativize(pcfg.currentRoot, j)[extension="md"].path;
+
+ if (!exists(targetFile) || lastModified(i) > lastModified(targetFile)) {
+ println("compiling [Index Markdown]");
+ output = compileMarkdown(i, pcfg[currentFile=i], exec, ind, sidebar_position=sidebar_position);
+
+ writeFile(targetFile,
+ "
+ '<}>"
+ );
+
+ if (details(list[str] xxx) <- output) {
+ // here we give the details list declared in `details` header
+ // on to compute the right sidebar_positions down for the nested
+ // concepts
+ nestedDtls = xxx;
+ }
+
+ errors = [e | err(e) <- output];
+ if (errors != []) {
+ writeBinaryValueFile(targetFile[extension="errors"], errors);
+ }
+ else {
+ remove(targetFile[extension="errors"]);
+ }
+ }
+ else {
+ println("reusing ");
+ if (exists(targetFile[extension="errors"])) {
+ errors = readBinaryValueFile(#list[Message], targetFile[extension="errors"]);
+ }
+ }
+ }
+ else {
+ generateIndexFile(d, pcfg, sidebar_position=sidebar_position);
+ }
+
+ return [
+ *errors,
+ *[*compile(s, pcfg, exec, ind, sidebar_position=sp)
+ | s <- d.ls
+ , !(s in pcfg.ignores)
+ , !(s in indexFiles)
+ , isDirectory(s) || s.extension in {"md","rsc","png","jpg","svg","jpeg", "html", "js"}
+ , int sp := indexOf(nestedDtls, capitalize(s[extension=""].file))
+ ]
+ ];
+}
+
+list[Message] generateIndexFile(loc d, PathConfig pcfg, int sidebar_position=-1) {
+ try {
+ p2r = pathToRoot(pcfg.currentRoot, d, pcfg.isPackageCourse);
+ title = (d == pcfg.currentRoot && d.file in {"src","rascal","api"}) ? "API" : d.file;
+
+ targetFile = pcfg.bin
+ + (pcfg.isPackageCourse ? "Packages/" : "")
+ + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file))
+ + relativize(pcfg.currentRoot, d).path
+ + "index.md"
+ ;
+
+ str slug = relativize(pcfg.bin, targetFile).parent.path;
+
+ writeFile(targetFile,
+ "---
+ 'title:
+ 'slug:
+ 'sidebar_position:
+ '<}>---
+ '
+ '
+ '* [](/Packages//<}>API<} else {><}>)<}>
+ '* [](/Packages//<}>API<} else {><}>/module_Index.md)<}>");
+ return [];
+ } catch IO(msg): {
+ return [error(msg, d)];
+ }
+}
+
+@synopsis{Translates Rascal source files to docusaurus markdown.}
+list[Message] compileRascalFile(loc m, PathConfig pcfg, CommandExecutor exec, Index ind) {
+ loc targetFile = pcfg.bin
+ + (pcfg.isPackageCourse ? "Packages/" : "")
+ + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file))
+ + relativize(pcfg.currentRoot, m)[extension="md"].path;
+
+ if (targetFile.file in {"index.md", "Index.md"}) {
+ // that would overwrite the actual index. Some modules can be named "Index.rsc or index.rsc"
+ // this underscore prefix is also reflected in the index builder of course!
+ targetFile.file = "module_Index.md";
+ }
+
+ errors = [];
+
+ if (!exists(targetFile) || lastModified(targetFile) < lastModified(m)) {
+ str parentSlug = (|path:///| + (pcfg.isPackageCourse ? "Packages/" : "")
+ + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file))
+ + relativize(pcfg.currentRoot, m).parent.path).path;
+
+ println("compiling [Rascal Source File]");
+ list[Output] output = generateAPIMarkdown(parentSlug, m, pcfg, exec, ind);
+
+ writeFile(targetFile,
+ "
+ '<}>"
+ );
+
+ errors = [e | err(e) <- output];
+ if (errors != []) {
+ writeBinaryValueFile(targetFile[extension="errors"], errors);
+ }
+ else {
+ remove(targetFile[extension="errors"]);
+ }
+ }
+ else {
+ println("reusing ");
+ if (exists(targetFile[extension="errors"])) {
+ errors = readBinaryValueFile(#list[Message], targetFile[extension="errors"]);
+ }
+ }
+
+ return errors;
+}
+
+@synopsis{This uses another nested directory listing to construct information for the TOC embedded in the current document.}
+list[str] createDetailsList(loc m, PathConfig pcfg)
+ = sort([ ":package:<}>module:<}>"
+ | loc d <- m.parent.ls, m != d, !(d in pcfg.ignores), d.file != "index.md", isDirectory(d) || d.extension in {"rsc", "md"}
+ ]);
+
+list[Message] compileMarkdownFile(loc m, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) {
+ order = createDetailsList(m, pcfg);
+
+ // turn A/B/B.md into A/B/index.md for better URLs in the end result (`A/B/`` is better than `A/B/B.html`)
+ m.file = (m.file == m.parent[extension="md"].file) ? "index.md" : m.file;
+
+ loc targetFile = pcfg.bin
+ + (pcfg.isPackageCourse ? "Packages/" : "")
+ + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file))
+ + relativize(pcfg.currentRoot, m)[extension="md"].path;
+
+ errors = [];
+
+ if (!exists(targetFile) || lastModified(m) > lastModified(targetFile)) {
+ println("compiling [Normal Markdown]");
+ list[Output] output = compileMarkdown(m, pcfg[currentFile=m], exec, ind, sidebar_position=sidebar_position) + [Output::empty()];
+
+ writeFile(targetFile,
+ "
+ '<}>"
+ );
+
+ errors = [e | err(e) <- output];
+ if (errors != []) {
+ writeBinaryValueFile(targetFile[extension="errors"], errors);
+ }
+ return errors;
+ }
+ else {
+ println("reusing ");
+ if (exists(targetFile[extension="errors"])) {
+ // keep reporting the errors of the previous run, for clarity's sake
+ return readBinaryValueFile(#list[Message], targetFile[extension="errors"]);
+ }
+ }
+
+ return [];
+}
+
+list[Output] compileMarkdown(loc m, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) {
+ order = createDetailsList(m, pcfg);
+
+ return compileMarkdown(readFileLines(m), 1, 0, pcfg[currentFile=m], exec, ind, order, sidebar_position=sidebar_position) + [Output::empty()];
+}
+
+@synopsis{Skip double quoted blocks}
+list[Output] compileMarkdown([str first:/^\s*``````/, *block, str second:/^``````/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = [
+ out(first),
+ *[out(b) | b <-block],
+ out(second),
+ *compileMarkdown(rest, line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+
+@synopsis{Include Rascal code from Rascal source files}
+list[Output] compileMarkdown([str first:/^\s*```rascal-include$/, *str components, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) {
+ return[
+ Output::empty(), // must have an empty line
+ out("```rascal "),
+ *[*prepareModuleForInclusion(item, /includeHeaders/ := rest1, /includeTests/ := rest1, pcfg) | item <- components],
+ Output::empty(),
+ out("```"),
+ *compileMarkdown(rest2, line + 1 + size(components) + 1, offset + length(first) + length(components), pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+}
+
+@synopsis{Include Rascal REPL commands literally and execute them as side-effects in the REPL without reporting output unless there are unexpected errors.}
+list[Output] compileMarkdown([str first:/^\s*```rascal-commands$/, *str block, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) {
+ str code = "
+ '<}>";
+
+ try {
+ commands = ([start[Commands]] code).top.commands;
+
+ if (/continue/ !:= rest1) {
+ exec.reset();
+ }
+
+ stderr = "";
+
+ for (EvalCommand c <- commands) {
+ output = exec.eval("");
+ stderr += output["application/rascal+stderr"]?"";
+ }
+
+ return [
+ Output::empty(), // must have an empty line
+ out("```rascal "),
+ *[out(l) | l <- block],
+ out("```"),
+ *[
+ out(":::danger"),
+ *[out(errLine) | errLine <- split("\n", stderr)],
+ out(":::")
+ | /errors/ !:= rest1, filterErrors(stderr) != ""
+ ],
+ *[err(error("rascal-commands block failed: ", pcfg.currentFile(offset, 1, , ))) | filterErrors(stderr) != ""],
+ *compileMarkdown(rest2, line + 1 + size(block) + 1, offset + length(first) + length(block), pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+ }
+ catch ParseError(x): {
+ return [err(error("parse error in rascal-commands block: ", pcfg.currentFile(offset, 1, , )))];
+ }
+}
+
+@synopsis{execute _rascal-shell_ blocks on the REPL}
+list[Output] compileMarkdown([str first:/^\s*```rascal-shell$/, *block, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = [ Output::empty(), // must have an empty line
+ out("```rascal-shell "),
+ *compileRascalShell(block, /error/ := rest1, /continue/ := rest1, line+1, offset + size(first) + 1, pcfg, exec, ind),
+ out("```"),
+ *compileMarkdown(rest2, line + 1 + size(block) + 1, offset + size(first) + length(block), pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+
+@synopsis{execute _rascal-shell-prepare_ blocks on the REPL}
+list[Output] compileMarkdown([str first:/^\s*```rascal-prepare$/, *block, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = [
+ *compileRascalShellPrepare(block, /continue/ := rest1, line+1, offset + size(first) + 1, pcfg, exec, ind),
+ *compileMarkdown(rest2, line + 1 + size(block) + 1, offset + size(first) + length(block), pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+
+@synopsis{inline an itemized list of details (collected from the details YAML section in the header)}
+list[Output] compileMarkdown([str first:/^\s*\(\(\(\s*TOC\s*\)\)\)\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = [
+ *[*compileMarkdown(["* (())"], line, offset, pcfg, exec, ind, []) | d <- dtls],
+ *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position)
+ ]
+ +
+ [
+ err(warning("TOC is empty. details section is missing from header?", pcfg.currentFile(offset, 1, , )))
+ | dtls == []
+ ];
+
+@synopsis{inline an itemized list of details (collected from the details YAML section in the header)}
+list[Output] compileMarkdown([str first:/^\s*\(\(\(\s*TODO\s*\)\)\)\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = [
+ out(":::caution"),
+ out("There is a \"TODO\" in the documentation source:"),
+ out("\t"),
+ out(first),
+ out(":::"),
+ err(warning("TODO: ", pcfg.currentFile(offset, 1, , ))),
+ *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position)
+ ];
+
+@synopsis{Inline example files literally, in Rascal loc notation, but do not compile further from there. Works only if positioned on a line by itself.}
+list[Output] compileMarkdown([str first:/^\s*\(\(\|\|\)\)\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) {
+ try {
+ return [
+ *[out(l) | str l <- split("\n", readFile(readTextValueString(#loc, "||")))],
+ *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position)
+ ];
+ }
+ catch value x: {
+ return [
+ err(error("Could not read for inclusion: ", pcfg.currentFile(offset, 1, , ))),
+ *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position)
+ ];
+ }
+}
+
+@synopsis{implement subscript syntax for [aeh-pr-vx] (the subscript alphabet is incomplete in unicode)}
+list[Output] compileMarkdown([/^~~$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = compileMarkdown([""]><}>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+
+@synopsis{detect unsupported subscripts}
+list[Output] compileMarkdown([/^~~$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1)
+ = [
+ err(error("Unsupported subscript character in ", pcfg.currentFile(offset, 1, , ))),
+ *compileMarkdown(["", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+
+@synopsis{Resolve labeled links}
+list[Output] compileMarkdown([/^\[\]\(\(\)\)$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) {
+ resolution = ind[removeSpaces(link)];
+ p2r = pathToRoot(pcfg.currentRoot, pcfg.currentFile, pcfg.isPackageCourse);
+
+ if (trim(title) == "") {
+ title = link;
+ }
+
+ switch (resolution) {
+ case {str u}: {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ case { }: {
+ if (/^\s+/ := link) {
+ // give this a second chance, in reverse
+ return compileMarkdown(["[]((-))", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+
+ return [
+ err(error("Broken concept link: ", pcfg.currentFile(offset, 1, ,))),
+ *compileMarkdown(["_() (broken link)_", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+ }
+ case {_, _, *_}: {
+ // ambiguous resolution, first try and resolve within the current course:
+ if ({str u} := ind[":"]) {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ else if ({str u} := ind["-"]) {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ // or we check if its one of the details of the current concept
+ else if ({str u} := ind[":-"]) {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+
+ return [
+ err(error("Ambiguous concept link: resolves to all of these: <}>", pcfg.currentFile(offset, 1, ,),
+ cause="Please choose from the following options to disambiguate: <- rangeR(ind, ind[removeSpaces(link)]), {_} := ind[k]) {>
+ ' resolves to <}>")),
+ *compileMarkdown([" **broken: (ambiguous)** ", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+ }
+ }
+
+ return [err(error("Unexpected state of link resolution for : ", pcfg.currentFile(offset, 1, ,)))];
+}
+
+@synopsis{Resolve unlabeled links}
+default list[Output] compileMarkdown([/^\(\(\)\)$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) {
+ resolution = ind[removeSpaces(link)];
+ p2r = pathToRoot(pcfg.currentRoot, pcfg.currentFile, pcfg.isPackageCourse);
+
+ switch (resolution) {
+ case {u}: {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ case { }: {
+ if (/^\s+/ := link) {
+ // give this a second chance, in reverse
+ return compileMarkdown(["((-))", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+
+ return [
+ err(error("Broken concept link: ", pcfg.currentFile(offset, 1, ,))),
+ *compileMarkdown(["_ (broken link)_", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position)
+ ];
+ }
+ case {str plink, /\/index\.md/}:
+ if (plink == qlink) {
+ return compileMarkdown(["[](/)", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ else {
+ fail;
+ }
+
+ case {_, _, *_}: {
+ // ambiguous resolution, first try and resolve within the current course:
+ if ({u} := ind[":"]) {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ else if ({u} := ind["-"]) {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position);
+ }
+ // or we check if its one of the details of the current concept
+ else if ({u} := ind[":-"]) {
+ u = /^\/assets/ := u ? u : "";
+ return compileMarkdown(["[]()