Lightweight publishing pipeline for digital learning content
Think: static-site-generator-style workflow for publishing to LMS platforms.
Diagram: Torrenzo workflow (Mermaid)
flowchart LR
subgraph src["Your subject content"]
direction TB
META["Subject metadata<br/>(learning outcomes, assessment details)"]
MOD["Module pages<br/>(Markdown / Word + images & assets)"]
ASS["Assessment briefs<br/>(Markdown + assets)"]
NOTES["Lecturer notes"]
end
T["Torrenzo"]
subgraph out["Build output"]
direction TB
HTML["HTML module pages"]
PDF["PDF assessment briefs"]
LNOTES["Lecturer notes"]
end
IMSCC["Common Cartridge<br/>(.imscc)"]
LMS[("Canvas / LMS")]
META --> T
MOD --> T
ASS --> T
NOTES --> T
T --> HTML
T --> PDF
T --> LNOTES
HTML ==> IMSCC
PDF ==> IMSCC
LNOTES ==> IMSCC
IMSCC ==>|import| LMS
HTML -.->|or paste| LMS
PDF -.->|or upload| LMS


Images: Editing a Torrenzo project using Obsidian for Markdown support. You can use any editor you prefer, including MS Word, with potential to support other formats in future.
- What Does It Do?
- Usage: GUI
- Usage: Command-Line Interface (CLI)
- Usage: Build Options (GUI & CLI)
- Configuration & Tags
- Prerequisites
- Populating Content
- Technical Stuff
Torrenzo traverses structured learning content directories and generates LMS-ready HTML module pages and PDF assessment briefs from Markdown, BibTeX, and other source material.
Torrenzo currently performs the following transformations:
| Input | Output |
|---|---|
assessments/assessment_<n>/ass_<n>_brief.md |
|
modules/module_<n>/mod_<n>_<seq>_<name>.md |
HTML |
modules/module_<n>/mod_<n>_<seq>_<name>.docx |
HTML |
See the demo/ directory for sample subject content, and demo/build/ for example output artefacts.
Torrenzo keeps learning content portable, readable, and version-controlled.
Instead of authoring material directly in a learning management system (LMS), content is written in semantic, plain-text formats such as Markdown and BibTeX. This approach enables:
- Consistent metadata defined once and reused everywhere (e.g., learning outcomes or assessment details)
- Version control using Git and other standard tools
- Clear separation of content and presentation
- Editor independence so you can write with any tool (Obsidian, VS Code, Vim, even MS Word + Styles)
- Machine-readable materials that automation tools and AI can locally analyse and update
- Extensible components for reusable interface elements across multiple pages
- Adaptable open-source tooling to extend or customise for your publishing workflow
Torrenzo includes a desktop application for point-and-click builds.
Image: The Torrenzo GUI running on Linux. The GUI is available for Linux, macOS, and Windows.
Features:
- Directory picker with history (remembers your subject folders)
- Build options as checkboxes
- Live build log displayed in the window
- Preview in Browser button opens the build output for review (with live-reload for
--watchmode) - Watch mode checkbox monitors source files and rebuilds automatically on change
- Diff button compares the local cartridge against a live Canvas export
Launch the GUI using one of the following methods:
- Prebuilt binary from the Releases page: https://github.com/tabreturn/torrenzo/releases
- From source (repo root):
python -m torrenzo_gui(ensure to install prerequisites first)
π‘ To start working on a Torrenzo project, it's easiest to use the demo as a scaffold, launch the GUI, and run a build to explore how everything works.
- Ensure to install prerequisites.
- Populate subject content (
outline.mdoroutline.yaml,assessments/, andmodules/). - Run Torrenzo from the repository root, passing your subject directory:
python -m torrenzo /path/to/your-subjectoutline.[md|yaml], assessments/, modules/, and build/ all resolve relative to the subject root. Torrenzo outputs everything (HTML, PDF, etc.) to build/ inside the subject directory. Only files whose sources have changed since the last build are regenerated; orphaned outputs are removed automatically. Use build options to override/control this behaviour.
-
By default, Torrenzo skips files whose outputs are already newer than their sources. Use
--forceto rebuild everything regardless, or--cleanto wipebuild/first and then do a full rebuild. -
Use
--optimize-assetsto optimise graphic assets. SVG optimisation uses the systemscourCLI tool (pip install scour); PNG optimisation requirespngquantoroxipnginstalled on your system. -
Use
--ccto output a Common Cartridge file for bulk-importing Canvas content.
- Use
--watchto monitor source files and rebuild them incrementally upon saving. In the GUI, ticking the --watch checkbox additionally retargets Preview in Browser to a live reload server that automatically refreshes the browser preview after each rebuild. For the CLI, use--live(which implies--watch) to start this HTTP server.
-
Use the Diff button (GUI) or
--diff LOCAL.imscc LIVE.imscc(CLI) to compare two cartridges and see what would change on import. This is useful when you want to apply targeted updates using the Canvas editor rather than importing an entire Common Cartridge. -
Use
--cache-bustto append a cache-busting suffix to any/assetsfilenames and their HTML references. This works around an intermittent Canvas issue where previously uploaded images stop rendering after a course re-import (I'm not sure why). Provide a custom tag or omit it for an auto-generated timestamp.
π‘ Each build writes (or appends to)
build/build-log.jsonlisting newly built files with timestamps. This provides one way to identify which files need updating in your LMS across multiple builds (if you're not using a common catridge and need to be selective).
π For a quick-reference summary of all available tags and their rendered output, see the Cheatsheet.
Use outline.[md|yaml] as the single source of metadata, formatted in YAML. Use Dataview-style tags in content, for example `=[[outline]].assessment.a1.weighting` or `=[[outline]].slo.a`
Alternatively, you can also just use the bare form without backticks or equals sign: [[outline]].assessment.a1.weighting or [[outline]].slo.a. Note, however, that the bare form won't render as a live preview in Obsidian.
Keys in outline.[md|yaml] define your subject metadata and automatically populate across all content via tags/placeholders.
- Subject:
subject.code,subject.title,subject.descriptor - SLOs:
Map underslowith codes (e.g.,slo.a) - Assessments:
Produce a full metadata table usingassessment.a1orassessment.a2, etc.
Use wiki-style [[...]] syntax to create links between modules and assessments. Torrenzo automatically derives the display label unless you explicitly provide one using a | character.
| Syntax | Renders as |
|---|---|
[[mod_01_02_oranges]] |
[Module 1.2: Oranges](mod_01_02_oranges.html) |
[[mod_01_02_oranges|Oranges]] |
[Oranges](mod_01_02_oranges.html) |
[[mod_02_02_mangoes|Mangoes]] |
[Mangoes](mod_02_02_mangoes.html) |
[[assessment_01]] |
[Assessment 1](assessment_01.html) |
[[assessment_01|Brief]] |
[Brief](assessment_01.html) |
Wiki links expand before Markdown rendering and before Common Cartridge export, so they work in the browser preview. However, Assessment link targets won't preview as those pages are built for the cartridge only.
π‘ Wiki links (
[[...]]) are syntactic sugar, and regular Markdown links like[Mangoes](../module_02/mod_02_02_mangoes.md)work too -- the build rewrites.mdto.htmlautomatically. Use raw<a href="path.md">if you must retain the.mdextension.
Images support optional Gemini-style CSS directives after a pipe (|), inlining styling on the resulting <img> element:
Output: <img src="assets/fruit.png" alt="Some fruit" style="max-width:300px;border-radius:8px">
Additionally, any heading in the brief tagged with [[cc-section]] (at any level) is rendered below the inline PDF, with its full branch of sub-sections and content included. Links to files in the assessment's assets/ directory (e.g. [exemplar.zip](assets/exemplar.zip)) will work as downloadable links in the Canvas HTML. Use [[cc-section|hide-in-pdf]] to hide the section in the generated PDF while keeping it visible in HTML.
Use [[includes|filename]] to inline reusable content from the includes/ directory. The included file's content resolves before tag processing, so includes can themselves contain tags and wiki links.
[[includes|referencing.md]]Includes resolve from {subject_root}/includes/, available to both assessments and modules. Changes to files in includes/ trigger automatic rebuilds.
Torrenzo includes built-in components for common page elements. Components use the bare [[...]] form (no backticks or equals sign):
| Tag | Description |
|---|---|
[[component.module-navigation]] |
Tabbed navigation linking to sibling sub-modules |
[[component.page-break]] |
Force a page break at this point in PDF files |
[[component.page-spacer]] |
Vertical gap (<p> </p>) for breathing room |
[[component.under-construction]] |
Amber banner; default text: "π§ Under construction" |
[[component.under-construction|Custom msg]] |
Amber banner with custom message: "π§ Custom msg" |
[[component.video|path/to/file]] |
Responsive 16:9 video player |
Each component renders to a <div data-tag="component-{name}">...</div> (or equivalent). Use the subject's modules/style/style.css (or assessments/style/style.css for PDF) to style these. Both HTML and PDF pipelines substitute all of the above tags.
π‘ The
[[component.video]]component is a bit of an exception, as it includes a fair amount of inline CSS. It also won't render videos in PDFs (for obvious reasons).
(to run Torrenzo or launch the GUI via CLI)
- Python 3.9+
- Google Chrome or Chromium (for PDF generation)
- Terminal environment of your choice
Clone or download the Torrenzo repository. All setup commands run from the Torrenzo repo root -- subject content typically lives separately (although the repo includes a demo).
To create and activate a virtual environment, then install dependencies:
python3 -m venv env
source env/bin/activate
pip install -r requirements.txtπ‘ Running the GUI from the CLI is useful when you don't have administrative privileges on your computer (but already have Python).
π‘ PDF generation uses your system's Chrome/Chromium. If Chrome is installed in a non-standard location, set the
PUPPETEER_EXECUTABLE_PATHenvironment variable (e.g.,export PUPPETEER_EXECUTABLE_PATH=/path/to/chrome).
The tool is filesystem-driven: file names and directory structure determine how content is processed.
Subject directory layout:
your-subject/
βββ assessments/ # assessment briefs β PDF
β βββ assessment_<n>/
β β βββ ass_<n>_brief.md
β β βββ assets/
β βββ style/ # branding (logo.svg, style.css, config.js)
βββ includes/ # reusable content snippets (inlined via [[includes|...]])
β βββ *.md
βββ modules/ # module content β HTML
β βββ module_00/ # overview / introductory content (landing page)
β β βββ mod_00_<seq>_<name>.[md|docx]
β β βββ assets/
β βββ module_<n>/
β β βββ mod_<n>_<seq>_<name>.[md|docx]
β β βββ assets/
β βββ style/ # stylesheet inlined into HTML output
β βββ references.bib # subject-level BibTeX references
βββ notes/ # lecturer-only notes (copied as-is, unpublished)
β βββ *.md (or any format)
βββ build/ # generated output
β βββ ...
βββ outline.[md|yaml] # subject configuration (YAML)
During the build process, Torrenzo reads metadata from outline.[md|yaml] (SLOs, etc.) and converts source content into:
- PDF assessment briefs
- LMS-ready HTML module pages (including separate activity pages)
π‘ To get started, you could simply duplicate the
demo/subject, rename it, perhaps move it, and use it as a starting point for developing new learning materials.
Subject content lives in two directories -- assessments/ and modules/. Torrenzo relies on strict naming conventions to locate and process files.
-
Define global metadata in
outline.[md|yaml](using YAML). Torrenzo injects these values wherever placeholders such as`=[[outline]].subject.title`appear in source Markdown or Word files. -
Define assessment briefs in
assessments/assessment_<n>/ass_<n>_brief.md. Place any assets the brief references (images, etc.) in the adjacentassets/directory. Note that Torrenzo will only process Markdown (not Word briefs). -
Store reference sources in
references.bib. This file uses BibTeX format; in-text citations use the[@refname]syntax. Torrenzo renders the corresponding references at the bottom of the page. -
Organise module files using the same pattern under
modules/module_<n>/. Each module contains:mod_<n>_<seq>_<name>.[md|docx]-- module page(s) (content and activities)assets/-- supporting files (images, etc.) used within the module- Note that Torrenzo extracts images from Word documents, so there is no need to place Word graphics in
assets/
Use modules/module_00/ for subject overview and introductory content (e.g., welcome page, student expectations, key documents). This typically serves as the landing page(s) content.
π‘ Module files follow the pattern
mod_<module_num>_<seq>_<name>.<ext>. For example:mod_01_01_introduction.md,mod_01_02_oranges.md, ormod_01_03_activities.md. Module folders accept an optional label suffix:module_01_citrus_fruits/becomes "Module 1 β Citrus Fruits" in the cartridge instead of "Module 1".
π‘ Torrenzo supports writing, organising, and navigating content in Obsidian. The
demo/subject includes an.obsidianconfiguration that you can copy to any working subject root -- then point a new vault at your subject directory to use it.
Torrenzo writes all output to build/. Module assets copy to build/modules_html/assets
Place lecturer-only materials (teaching notes, facilitation guides, etc.) in notes/. These files copy to build/lecturer_notes/ retaining their original format -- no conversion applies (.md stays .md, .docx stays .docx, and so forth). When exporting a Common Cartridge (via --cc), lecturer notes end up in the .imscc package under an unpublished module, hidden from students.
An optional global stylesheet lives at modules/style/style.css. Its inlines CSS into HTML output so styling survives LMS copy-paste and cartidge imports without requiring additional stylesheets in the target LMS.
Universal assessment branding assets live in assessments/style/. On each run, the build injects logo.svg into the PDF header. Replace logo.svg (must be an SVG) to use a different logo, and configure styling and header/footer elements via the style.css and config.js
π‘ Each stylesheet and
config.jsincludes a metadata block at the top (Theme,Output,Version,Modified). It's best practice to update these when you customise styles, so theming is easier to track across projects.
This section is intended for developers and contributors.
Torrenzo uses a plugin-style architecture with an extensible set of transformers:
| Transformer | Conversion |
|---|---|
torrenzo/torrenzo_engine/renderers/bib_to_html.py |
BibTeX β HTML |
torrenzo/torrenzo_engine/renderers/docx_to_html.py |
MS Word β HTML |
torrenzo/torrenzo_engine/renderers/md_to_html.py |
Markdown β HTML |
torrenzo/torrenzo_engine/renderers/md_to_pdf.py |
Markdown β PDF |
π‘ MS Word is not a priority source format, so it has received comparatively little attention. As a matter of preference, the Torrenzo contributor(s) work exclusively in Markdown. While
modulescontent can handle Word documents,assessmentssupport is limited to Markdown, as Word-formatted PDF briefs require manual exporting to ensure formatting accuracy.
Torrenzo supports additional transformers without modifying the core pipeline. Developers should extend it to new targets (e.g., Marp slides) without expanding the CLI driver. Potential candidates include:
- Marp
.mdβ PDF (slide decks) - Extended Markdown features for module pages (accordions, navigation tabs, and other LMS-specific markup)
- Really, the limit is your imagination and whatever an LMS can handle ...
Pass --cc to generate an IMS Common Cartridge package alongside the normal build output. The .imscc file bundles all module pages as Canvas WikiPages (with inter-page navigation and asset paths rewritten to Canvas's $WIKI_REFERENCE$ / $IMS-CC-FILEBASE$ tokens).
Importing into Canvas:
- Navigate to the target course and select Settings β Import Course Content.
- Set Content Type to Common Cartridge 1.x Package, choose the
.imsccfile, and click Import. - Canvas will prompt you to select All content or Specific content. Importing all content populates:
- Modules -- Grouped, numbered modules containing related pages; assessments appear in a separate Assessments module.
- Pages -- All module pages appear as WikiPages. The first page in
Module_00includes afront_pagemeta tag, though you may need to set it manually in Canvas via Pages β View All Pages β β β Use as Front Page. - Assignments -- A submission point with its total marks, weighting, and configured rubric (parsed from the last table in the brief markdown).
- Files -- assessment PDFs and image assets uploaded to course Files.
- Lecturer Notes -- Lecturer-only materials set to
unpublished(hidden from students); notes retain their original format.
π‘ Observation note: When importing cartridges into Canvas, module content is overwritten unless it has been modified in the Canvas editor. However, assets may be duplicated during the process. Recommended approach: before bulk importing, delete all items in Files (in Canvas), except for the
course_imagefolder.
Pass --diff to compare two .imscc files -- typically your local build against a Canvas export:
python -m torrenzo demo --diff build/FRU101.imscc canvas-export.imsccTo generate a log, redirect output to a file using > diff.log (or preffered filename).
The Diff feature allows you to compare your active project against an exported Canvas file. This is especially helpful when you need to be selective about updating content, as importing a massive cartridge for every minor tweak can be inefficient, messy, or even restricted by institutional permissions. Diff groups results into three categories:
| Category | Marker | Description |
|---|---|---|
| CHANGES | * |
Content, points, or checksum differences |
| LIVE-ONLY | β |
Items not in local build (prefixed (wikipage), (assessment), (asset)) |
| REBUILT, SAME CONTENT | β |
PDFs re-rendered but identical after stripping timestamps |
Additional live-only Canvas artifacts (LTI links, QTI quizzes, course image) appear under LIVE-ONLY with a β’ bullet. You can jump to more technical info on Diff's workings
- WikiPages compared by normalized body content (Normalizes away all: HTML entities, inline styles, GUIDs, attribute ordering, and Canvas-specific formatting)
- Assessment PDFs compared by text-content checksum with build timestamps/versions stripped
- Other assets (SVGs, images) compared by SHA-256 checksum
- File rename quirks (from Canvas import-export roundtrips) handled automatically
π‘ Use
--diff-verboseto see full content diffs for modified WikiPages.