The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

App::SlideServer - Mojo web server that serves slides and websocket

VERSION

version 0.002

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. The slides then use the provided JavaScript to create 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).

On startup, the application upgrades your slides to a proper HTML structure (see "HTML SPECIFICATION") possibly by first running it through a Markdown renderer if you provided the slides as markdown instead of HTML. 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.

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_chagned' value and the content has not changed, this returns undef.

markdown_to_html

  $html= $app->markdown_to_html($md);

This is a simple wrapper around Markdown::Hoedown with most of the syntax options enabled. You can substitute any markdown processor you like by overriding this method in a subclass.

extract_slides_dom

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 "HTML SPECIFICATION". It returns one Mojo::DOM object for the top-level page, and one Mojo::DOM object for each detected slide, as a list.

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 <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.

update_published_state

  $app->update_published_state( $k => $v, ...)

Apply any number of key/value pairs to the "published_state", and then push it out to all "viewers".

EVENT 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.

  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 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 which exact steps 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).

To ease the common scenario of assigning steps to a bullet list, you may put the .auto-step class on any element, and its child elements will receive sequential step numbers starting from the step value of that element, defaulting to 1.

Each div.slide may contain a div.notes, which is only visible to the presenter. (TODO: currently this is only enforced with css, but should be handled in the back-end)

Convenient Translations

If you don't provide a div.slides element, one will be added under body. If you don't have a body tag, one will be added for you as well.

If you put elements other than div.slide under the div.slides element in your source file, they will be automatically broken into slides as follows:

<h1> <h2> <h3>

These each trigger the start of a new slide

<hr>

This begins a new slide while deleting the <hr> element, useful when you have slides without headers.

AUTHOR

Michael Conrad <mike@nrdvana.net>

COPYRIGHT AND LICENSE

This software is copyright (c) 2023 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.