NAME
App::SlideServer - Mojo web server that serves slides and synchronization events
VERSION
version 0.003
SYNOPSIS
use App::SlideServer;
my $app= App::SlideServer->new(\%opts);
$app->start(qw( daemon -l http://*:2000 ));
DESCRIPTION
This class is a fairly simple Mojo web application that serves a small directory of files, one of which is a Markdown or HTML document containing your slides. It pre-processes your slides to make them into a more complete HTML document, and then injects JavaScript to render them as a presentation similar to other popular software, and a user interface for the presenter. As a bonus, you can make any number of connections to the server and synchronize the slide show over a websocket, allowing viewers to follow the presenter, or the presenter to flip slides from a different device than is connected to the projector (like a tablet or phone).
The official HTML structure of the slides is described below. On startup, the application attempts to "DWIM" to upgrade your Markdown or loose HTML/XML notations into the official HTML structure, by applying the transformations described below. The goal is to specify your slides with a minimum of hassle, but also allow you to do the full range of what HTML is capable of, if you need to. It then inspects the HTML and breaks it apart into one or more slides.
You may then start the Mojo application as a webserver, or whatever else you wanted to do with the Mojo API.
The application comes with a collection of web assets that render your HTML to fit fullscreen in a browser window, and to provide a user interface to the presenter that shows navigation buttons and private notes for giving the presentation.
CONSTRUCTOR
This is a standard Mojolicious object with a Mojo::Base ->new constructor.
ATTRIBUTES
serve_dir
This is a Mojo::File object of the diretory containing templates and public files. Public files live under $serve_dir/public. See "slides_source_file" for the location of your slides.
The default is $self->home (Mojolicious project root dir)
slides_source_file
Specify the actual path to the file containing your slides. The default is to use the first existing file of:
* $serve_dir/slides.html
* $serve_dir/slides.md
* $serve_dir/public/slides.html
* $serve_dir/public/slides.md
Note that files in public/ can be downloaded as-is by the public at any time. ...but maybe that's what you want.
slides_source_monitor
If monitor_source_changes is enabled, this will be an instance of Linux::Inotify2::Watch.
share_dir
This is a Mojo::File object of the directory containing the web assets that come with App::SlideServer. The default uses File::ShareDir and should 'just work' for you.
presenter_key
This is a secret string that only you (the presenter) should know. It is your password to let the server know that your browser is the one that should be controlling the presentation.
If you don't initialize this, it defaults to a random value which will be printed on STDOUT where (presumably) only you can see it.
viewers
A hashref of ID => $context where $context is the Mojo context object for the client's websocket connection, and ID is the request ID for that websocket. This is updated as clients connect or disconnect.
published_state
A hashref of various data which has been broadcast to all viewers. This keeps track of things like the current slide, but you can extend it however you like if you want to add features to the client javascript. Use "update_published_state" to make changes to it.
page_dom
This is a Mojo::DOM object holding the page that is served as "/" containing the client javascript and css (but not any of the slides). This is a cached output of "build_slides" and may be rebuilt at any time.
slides_dom
This is an arrayref of the individual slides (Mojo::DOM objects) that the application serves. This is a cached output of "build_slides" and may be rebuilt at any time.
cache_token
A value (usually an mtime) that is used by "load_slides_html" to determine if the source content has changed. You can clear this value to force "build_slides" to perform a rebuild.
METHODS
build_slides
This calls "load_slides_html" (which calls markdown_to_html if your source is markdown) then calls "extract_slides_dom" to break the HTML into Mojo::DOM objects (and restructure shorthand notations into proper HTML), then calls "merge_page_assets" to augment the top-level page with the web assets like javascript and css needed for the client slide UI, then stores this result in "page_dom" and "slides_dom" and returns $self. It throws exceptions if it fails, leaving previous results intact.
You can override any of those methods in a subclass to customize this process.
This method is called automatically at startup and any time the mtime of your source file changes. (detected lazily when serving '/')
load_slides_html
$html= $app->load_slides_html;
($html, $token)= $app->load_slides_html(if_changed => $token)
Reads "slides_source_file", calls "markdown_to_html" if it was markdown, and returns the content as a string of characters, not bytes.
In list context, this returns both the content and a token value that can be used to test if the content changed (usually file mtime) on the next call using the 'if_changed' option. If you pass the 'if_changed' value and the content has not changed, this returns undef.
monitor_source_changes
$ss->monitor_source_changes; # enable
$ss->monitor_source_changes(0); # disable
Calling this method requires Linux::Inotify2 to be installed, which is not a hard dependency of App::SlideServer. It sets up a notification on the source file of your slides, so that changes will be propagated immediately to the browser as soon as you save it.
markdown_processor
This attribute holds a coderef that receives a string of markdown and returns a string of HTML. The lazy-built default looks for the first available module among Markdown::Perl or Text::Markdown::Hoedown. It enables most of the "Git Flavor" available from the available module. You can substitute any markdown processor you like by supplying this attribute.
markdown_to_html
$html= $app->markdown_to_html($md);
Applies a string of markdown to the "markdown_processor" and returns the HTML. This also truncates the Markdown at an __END__ marker, if found.
extract_slides_dom
($dom, @slides)= $app->extract_slides_dom($html, %options);
This function takes loose shorthand HTML (or full HTML) and splits out the slides content while also upgrading them to full HTML structure according to TRANSFORMATIONS. It returns one Mojo::DOM object for the top-level page, and one Mojo::DOM object for each detected slide, as a list.
This does not accept Markdown; if the source was Markdown the markdown_to_html will have been applied first.
merge_page_assets
$merged_dom= $app->merge_page_assets($src_dom);
This starts with the page_template.html shipped with the module, then adds any (post transformation) <head> tags from the source file, then merges the body or div.slides tags from the source file. It is assumed the slides have already been removed from $src_dom by "extract_slides_dom".
update_published_state
$app->update_published_state( $k => $v, ...)
Apply any number of key/value pairs to the "published_state", and then broadcast it to all "viewers".
ACTION METHODS
serve_page
GET /
Returns the root page, without any slides.
serve_slides
GET /slides
GET /slides?i=5
Returns HTML for one or more slides.
init_slidelink
GET /slidelink.io
Open a websocket connection. This method determines whether the new connection is a presenter or not, and then sets up the events and adds it to "viewers" and pushes out a copy of "published_state" to the new client.
on_viewer_message
Handle an incoming message from a websocket.
on_viewer_disconnect
Handle a disconnect event form a websocket.
EXPORTS
mojo2logany
->new(log => mojo2logany);
->new(log => mojo2logany("channel::name"));
->new(log => mojo2logany($logger));
This is a convenience function that returns a Mojo::Log object which passes all logging events to a Log::Any logging channel. I like the Log::Any ecosystem and it's a little messy to redirect the logs without a utility function.
HTML SPECIFICATION
The basic shape of the HTML is:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div class="slides">
<div class="slide">
<h3>Title of Slide</h3>
<ul>
<li data-step="1"> ...
<li data-step="2"> ...
<li data-step="3"> ...
</ul>
</div>
<div class="slide">
<h3>Title of Slide</h3>
<div data-step="1-1">
...
</div>
</div>
</div>
</body>
</html>
The page must contain a <div class="slides"> somewhere inside the <body>. All slides are <div class="slide"> and must occur as direct children of the div.slides element.
Inside a div.slide element, the elements may belong to iterative steps. This is indicated using the data-step="..." attribute. The notation for that attribute can be a single integer, meaning the element becomes visible at that step data-step="2", or a list of ranges of integers indicating the exact steps on which the element will be visible data-step="2-3,5-5". Step number 0 is initially visible when the slide comes up (so you don't need to specify number 0 anywhere, because it's already visible).
TRANSFORMATIONS
Aside from converting Markdown to HTML, the following convenient transformations are available.
HEAD-things
The following tags are automatically moved to <head>:
* head (contents get merged)
* title
* link
* base
* meta
* style (conditionally, TODO)
* script (conditionally)
Script tags with "src=" attribute, and script tags before the first slide get automatically moved to the <head> tag. <script> tags with code inline found within a slide remain at that position in the DOM. Eventually, the script will only be executed after the contents of the slide have been transmitted to the viewer, though that feature is TODO.
Style tags found within the content of a slide will eventually be scoped to that slide, by dynamically rewriting the styles, though this is also a TODO feature. Currently, just put an ID on your slide and write the css rules manually scoped to that ID.
Slide-Splitting
If you don't provide a top-level <body> or <div class="slides">, they will be added for you, wrapping all your provided content.
Each slide is a <div class="slide"> element, but to avoid the boilerplate of writing that for every slide, the following tags will generate a new slide when encountered within the <div class="slides"> element:
* h1
* h2
* h3
* hr (hr element gets removed)
This plays nicely with Markdown, so you can define slides as
## Slide Title
Content
## Next Slide
Content2
------
Slide content with no heading
### Next Slide
Content
which get converted to this HTML:
<h2>Slide Title</h2>
Content
<h2>Next Slide</h2>
Content2
<hr>
Slide content with no heading
<h3>Next Slide</h3>
Content
Which then gets transformed to:
<div class="slide">
<h2>Slide Title</h2>
Content
</div>
<div class="slide">
<h2>Next Slide</h2>
Content2
</div>
<div class="slide">
<h2>Slide Title</h2>
Content
</div>
<div class="slide">
<h2>Slide Title</h2>
Content
</div>
<notes>
Each <div class="slide"> may contain a <div class="notes">, which is only visible to the presenter. (TODO: currently this is only enforced with css, but should be handled in the back-end)
Since you probably want one of these on every slide, the shorthand of <notes>...</notes> gets upgraded to <div class="notes">...</div>.
In the default CSS, notes are handled like <pre> tags, so newlines are preserved.
Step Animations
You can animate your slides with the "step" system. HTML elements can be assigned the data-step="..." attribute. This attribute is a list of which animation frame numbers the element should be visible on.
data-step="1" # Visible from step 1 onward
data-step="1-3" # Visible on steps 1, 2, and 3, hidden afterward
data-step="1,3" # Visible on steps 1 and 3 only
data-step="1-2,4-5" # Visible on steps 1 and 2, and 4 and 5, else hidden
To ease the common scenario of assigning animation steps to a bullet list, you may put class="auto-step" on any element, and its child elements will receive sequential step numbers starting from the step value of that element, defaulting to 1. Additionally, <ul> and <ol> tags automatically get class "auto-step" added if no other class exists.
AUTHOR
Michael Conrad <mike@nrdvana.net>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Michael Conrad.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.