Removes slidy for presentations and replace with frise.js

Chris Pollett [2024-07-23 00:Jul:rd]
Removes slidy for presentations and replace with frise.js
Filename
src/controllers/StaticController.php
src/controllers/components/SocialComponent.php
src/css/frise.css
src/css/slidy.css
src/library/WikiParser.php
src/scripts/frise.js
src/scripts/slidy.js
src/views/layouts/WebLayout.php
diff --git a/src/controllers/StaticController.php b/src/controllers/StaticController.php
index 79446e6f1..70cf6fedf 100644
--- a/src/controllers/StaticController.php
+++ b/src/controllers/StaticController.php
@@ -269,8 +269,8 @@ EOD;
                 $this->component("social")->sortWikiResources($data);
             } else if ($head_info['page_type'] == 'presentation') {
                 $data['page_type'] = 'presentation';
-                $data['INCLUDE_SCRIPTS'][] =  "slidy";
-                $data['INCLUDE_STYLES'][] =  "slidy";
+                $data['INCLUDE_SCRIPTS'][] =  "frise";
+                $data['INCLUDE_STYLES'][] =  "frise";
             }
         }
         if ((!empty($data['PAGE']) &&
diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php
index 2a50a7c87..b05f00e84 100644
--- a/src/controllers/components/SocialComponent.php
+++ b/src/controllers/components/SocialComponent.php
@@ -4238,8 +4238,8 @@ EOD;
         } else if ($data["HEAD"]['page_type'] == 'presentation' &&
             $data['CONTROLLER'] == 'group') {
             $data['page_type'] = 'presentation';
-            $data['INCLUDE_SCRIPTS'][] =  "slidy";
-            $data['INCLUDE_STYLES'][] =  "slidy";
+            $data['INCLUDE_SCRIPTS'][] =  "frise";
+            $data['INCLUDE_STYLES'][] =  "frise";
         }
     }
     /**
diff --git a/src/css/frise.css b/src/css/frise.css
new file mode 100644
index 000000000..4333931f8
--- /dev/null
+++ b/src/css/frise.css
@@ -0,0 +1,999 @@
+/**
+ * FRISE (FRee Interactive Story Engine)
+ * A light-weight engine for writing interactive fiction and games.
+ *
+ * Copyright 2022-2024 Christopher Pollett chris@pollett.org
+ *
+ * @license
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * @file The CSS code used to style the elements used by a game
+ * @author Chris Pollett
+ * @link https://www.frise.org/
+ * @copyright 2022 - 2024
+ */
+/* make sure game tags we don't want to display are not displayed
+ */
+x-action,
+x-game,
+x-dollhouse,
+x-slides,
+x-object,
+x-location
+{
+    display: none;
+}
+/**
+ * Prevents border around canvas's that respond to keyboard events such as
+ * those made by dollhouses
+ */
+canvas:focus
+{
+    outline:none;
+}
+/*
+ * General useful classes
+ */
+.none
+{
+    display: none;
+}
+.hidden
+{
+    visibility: hidden;
+}
+.block
+{
+    display: block;
+}
+.inline-block
+{
+    display: inline-block;
+}
+.inline
+{
+    display: inline;
+}
+.float-right
+{
+    float: right;
+}
+.float-left
+{
+    float: left;
+}
+.float-opposite
+{
+    float: right;
+}
+.rtl .float-opposite
+{
+    float: left;
+}
+.float-same
+{
+    float: left;
+}
+.rtl .float-same
+{
+    float: right;
+}
+.left
+{
+    text-align: left;
+}
+.right
+{
+    text-align: right;
+}
+.opposite
+{
+    text-align: left;
+}
+.rtl .opposite
+{
+    text-align: right;
+}
+.center
+{
+    text-align: center;
+}
+.fit-content
+{
+    width: fit-content;
+}
+.rounded
+{
+    border-radius: 20px;
+}
+.rounded-top
+{
+    border-radius: 20px 20px 0 0;
+}
+.small-width
+{
+    width: 100px;
+}
+.mobile .small-width
+{
+    width: 75px;
+}
+.medium-width
+{
+    width: 150px;
+}
+.mobile .medium-width
+{
+    width: 100px;
+}
+.large-width
+{
+    width: 200px;
+}
+.mobile .large-width
+{
+    width: 150px;
+}
+.thin-border
+{
+    border: solid 1px black;
+}
+.medium-border
+{
+    border: solid 2px black;
+}
+.thick-border
+{
+    border: solid 4px black;
+}
+/**
+ * Special Effects, Animation tags and CSS classes
+ */
+ .repeating
+ {
+     animation-iteration-count: infinite;
+ }
+/* classes for animation count*/
+.once
+{
+    animation-iteration-count: 1;
+}
+.twice
+{
+    animation-iteration-count: 2;
+}
+.thrice
+{
+    animation-iteration-count: 3;
+}
+.four-times
+{
+    animation-iteration-count: 4;
+}
+.five-times
+{
+    animation-iteration-count: 5;
+}
+.ten-times
+{
+    animation-iteration-count: 10;
+}
+.infinite
+{
+    animation-iteration-count: infinite;
+}
+/* classes for animation duration */
+.half-sec
+{
+    animation-duration: 0.5s;
+}
+.second
+{
+    animation-duration: 1s;
+}
+.two-second
+{
+    animation-duration: 2s;
+}
+.three-second
+{
+    animation-duration: 3s;
+}
+.five-second
+{
+    animation-duration: 5s;
+}
+.ten-second
+{
+    animation-duration: 10s;
+}
+.top-left-origin
+{
+    transform-origin: 0 0;
+}
+.top-right-origin
+{
+    transform-origin: 100% 0;
+}
+.top-mid-origin
+{
+    transform-origin: 50% 0;
+}
+.mid-left-origin
+{
+    transform-origin: 0 50%;
+}
+.mid-mid-origin,
+.center-origin
+{
+    transform-origin: 50% 50%;
+}
+.mid-right-origin
+{
+    transform-origin: 100% 50%;
+}
+.bottom-right-origin
+{
+    transform-origin: 100% 100%;
+}
+.bottom-left-origin
+{
+    transform-origin: 0 100%;
+}
+.bottom-mid-origin
+{
+    transform-origin: 50% 100%;
+}
+x-outline,
+.outline
+{
+    color: white;
+    text-shadow:
+        -1px -1px 0 black,
+        1px -1px 0 black,
+        -1px 1px 0 black,
+        1px 1px 0 black;
+}
+x-mild-blur,
+.mild-blur
+{
+    filter: blur(1px);
+}
+x-blink,
+.blink
+{
+    animation: blinker 1.2s ease infinite;
+}
+@keyframes blinker {
+    50% {
+        opacity: 0;
+    }
+}
+x-blur,
+x-unblurable,
+.blur
+{
+    filter: blur(3px);
+}
+x-unblurable:active,
+x-unblurable:hover,
+.hover-unblur:active,
+.hover-unblur:hover
+{
+    filter: blur(0);
+    transition: filter 1s ease-in;
+}
+x-heavy-blur,
+.heavy-blur
+{
+    filter: blur(5px);
+}
+
+x-blur,
+x-heavy-blur,
+x-mild-blur,
+x-outline,
+x-unblurable
+{
+    display: inline;
+}
+
+x-condense,
+.condense
+{
+    display: inline-block;
+    transform: scaleX(0.5);
+    transform-origin: 0;
+}
+x-expand,
+.expand
+{
+    display: inline-block;
+    transform: scaleX(2);
+    transform-origin: 0;
+}
+x-emboss,
+.emboss
+{
+    box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3);
+    display: inline-block;
+    text-shadow: -2px -2px 1px rgba(255, 255, 255, 0.6),
+        3px 3px 3px rgba(0, 0, 0, 0.4);
+}
+.rewind
+{
+    animation-direction: reverse !important;
+}
+@keyframes fade-in
+{
+    0% {opacity: 0}
+    to {opacity: 1}
+}
+x-fade-in,
+.fade-in
+{
+    animation: fade-in .5s ease-in-out forwards;
+    opacity: 1;
+}
+@keyframes fade-out
+{
+    0% {opacity: 1}
+    to {opacity: 0}
+}
+x-fade-out,
+.fade-out
+{
+    animation: fade-out .5s ease-in-out forwards;
+    opacity: 0;
+}
+@keyframes fade-in-out
+{
+    0%,
+    to {opacity: 0}
+    50% {opacity: 1}
+}
+x-fade-in-out,
+.fade-in-out
+{
+    animation: fade-in-out 1s ease-in-out infinite alternate;
+}
+@keyframes scale-in
+{
+    0% {transform: scale(1,1);}
+    to {transform: scale(0,0);}
+}
+x-scale-in,
+.scale-in
+{
+    animation: scale-in .5s ease-in-out forwards;
+    display:block;
+    height: 100%;
+}
+@keyframes scale-out
+{
+    0% {transform: scale(0,0);}
+    to {transform: scale(1,1);}
+}
+x-scale-out,
+.scale-out
+{
+    animation: scale-out .5s ease-in-out forwards;
+    display:block;
+    height: 100%;
+}
+@keyframes slide-in-top
+{
+    0% {transform: translate(0, -120%);}
+    to {transform: translate(0,0);}
+}
+x-slide-in-top,
+.slide-in-top
+{
+    animation: slide-in-top .5s ease-in-out forwards;
+    display:block;
+    height: 100%;
+}
+@keyframes slide-out-top
+{
+    0% {transform: translate(0, 0);}
+    to {transform: translate(0, -120%);}
+}
+x-slide-out-top,
+.slide-out-top
+{
+    animation: slide-out-top .5s ease-in-out forwards;
+    display:block;
+    height: 100%;
+}
+@keyframes slide-in-bottom
+{
+    0% {transform: translate(0, 120%);}
+    to {transform: translate(0,0);}
+}
+x-slide-in-bottom,
+.slide-in-bottom
+{
+    animation: slide-in-bottom .5s ease-in-out forwards;
+    display:block;
+    height: 100%;
+}
+@keyframes slide-out-bottom
+{
+    0% {transform: translate(0, 0);}
+    to {transform: translate(0, 120%);}
+}
+x-slide-out-bottom,
+.slide-out-bottom
+{
+    animation: slide-out-bottom .5s ease-in-out forwards;
+    display:block;
+    height: 100%;
+}
+@keyframes slide-in-left
+{
+    0% {transform: translate(-120%, 0);}
+    to {transform: translate(0,0);}
+}
+x-slide-in-left,
+.slide-in-left
+{
+    animation: slide-in-left .5s ease-in-out forwards;;
+    display:block;
+    width: 100%;
+}
+@keyframes slide-out-left
+{
+    0% {transform: translate(0, 0);}
+    to {transform: translate(-120%, 0);}
+}
+x-slide-out-left,
+.slide-out-left
+{
+    animation: slide-out-left .5s ease-in-out forwards;;
+    display:block;
+    width: 100%;
+}
+@keyframes slide-in-right
+{
+    0% {transform: translate(120%, 0);}
+    to {transform: translate(0,0);}
+}
+x-slide-in-right,
+.slide-in-right
+{
+    animation: slide-in-right .5s ease-in-out;
+    display:block;
+    width: 100%;
+}
+@keyframes slide-out-right
+{
+    0% {transform: translate(0, 0);}
+    to {transform: translate(120%, 0);}
+}
+x-slide-out-right,
+.slide-out-right
+{
+    animation: slide-out-right .5s ease-in-out forwards;
+    display:block;
+    width: 100%;
+}
+x-vertical-mirror,
+.vertical-mirror
+{
+    display: inline-block;
+    transform: scaleY(-1);
+}
+x-mirror,
+.mirror
+{
+    display: inline-block;
+    transform: scaleX(-1);
+}
+@keyframes pulse
+{
+    0%  { transform: scale(0.7, 0.7);}
+    20% { transform: scale(1.25, 1.25); }
+    40% { transform: scale(0.8125, 0.8125); }
+    60% { transform: scale(1.125, 1.125); }
+    80% { transform: scale(0.9, 0.9); }
+    to  { transform: scale(1, 1); }
+}
+x-pulse,
+.pulse
+{
+    animation: pulse 0.8s;
+    display: inline-block;
+}
+@keyframes rumble
+{
+    50% {
+        transform: translateY(-4px)
+    }
+}
+x-rumble,
+.rumble
+{
+    animation: rumble linear 0.15s 0s infinite;
+    display:inline-block;
+}
+@keyframes shudder
+{
+    50% { transform: translateX(4px) }
+}
+x-shudder,
+.shudder
+{
+    animation: shudder linear 0.15s 0s infinite;
+    display: inline-block;
+}
+x-upside-down,
+.upside-down
+{
+    display: inline-block;
+    transform: rotate(180deg);
+}
+/*
+ * Styles specific to the game content and nav areas
+ */
+#game-content
+{
+    font-family: "Oswald", serif;
+    font-size: 18pt;
+    height: 100%;
+    left: 300px;
+    line-height: 1.6;
+    overflow-x: scroll;
+    overflow-y: scroll;
+    position: fixed;
+    transition: left .25s ease-in;
+    top: 0px;
+    padding-top: 23px;
+    width: calc(100% - 320px);
+}
+.rtl
+{
+    direction: rtl;
+}
+.rtl #game-content
+{
+    left: unset;
+    right: 300px;
+    transition: right .25s ease-in;
+}
+.no-nav #game-content
+{
+    left: 20px;
+    right: 20px;
+    transition: unset;
+    width: calc(100% - 40px);
+}
+#main-nav,
+#main-bar
+{
+    background: linear-gradient(to right, white, 97%, lightgray);
+    height: 100%;
+    left: 0;
+    overflow-y: scroll;
+    position: fixed;
+    text-align: center;
+    transition: left .25s ease-in;
+    top: 0px;
+    width: 240px;
+    z-index: 0;
+}
+.rtl #main-nav,
+.rtl #main-bar
+{
+    background: linear-gradient(to left, white, 97%, lightgray);
+    left: unset;
+    right: 0;
+    transition: right .25s ease-in;
+}
+#main-bar
+{
+    background: white;
+    height: 50px;
+    text-align: left;
+    top: 0;
+    width: 50px;
+    z-index: 2;
+}
+.rtl #main-bar
+{
+    background: white;
+}
+.mobile #main-bar
+{
+    background: white;
+    height: 50px;
+    top: 0;
+    width: 50px;
+    z-index: 2;
+}
+.rtl #main-bar
+{
+    text-align: right;
+}
+#main-nav button,
+#main-bar button
+{
+    border-radius: 10px;
+    font-size: 21pt;
+    padding: 2px 5px 5px 5px;
+    width: 40px;
+}
+.mobile #main-nav button,
+.mobile #main-bar button
+{
+    font-size: 18pt;
+    height: 50px;
+    margin: 2px;
+    padding: 8px 10px 10px 10px;
+    width: 40px;
+}
+#main-nav h1
+{
+    margin: 8px;
+}
+#main-nav x-button,
+#main-nav input[type="range"]
+{
+    width: 170px;
+}
+#main-nav .nav-label
+{
+    font-size: 14pt;
+    margin: auto;
+    text-align: left;
+    width: 170px;
+}
+.rtl #main-nav .nav-label
+{
+    text-align: right;
+}
+#main-nav .nav-info
+{
+    font-size: 14pt;
+    margin: 0px auto 15px auto;
+    text-align: left;
+    width: 170px;
+}
+.rtl #main-nav .nav-info
+{
+    text-align: right;
+}
+.footer-space {
+    height: 1in;
+}
+/*
+ * Styles specific to when we are using frise for a slide show
+ */
+.slides #game-content
+{
+    left: 0;
+    right: 0;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+}
+div.slide
+{
+    background-color: white;
+    color: black;
+    margin: 0;
+    padding: 20px;
+    font-family: "Gill Sans MT", "Gill Sans", GillSans, sans-serif;
+    font-size: 120%;
+    height: calc(95% - 50px);
+}
+.all div.slide
+{
+    height: fit-content;
+}
+.slide h1
+{
+    line-height: 1.1;
+}
+.slide pre
+{
+    background-color: #EEE;
+    border-style: solid;
+    border-width: thin;
+    border-left-width: 1em;
+    border-color: #9AD;
+    color: #048;
+    font-size: 80%;
+    font-weight: bold;
+    padding: 0.2em 1em 0.2em 1em;
+
+}
+.slide a img
+{
+    border-width: 0;
+    border-style: none;
+}
+.slide a:link,
+.slide a:visited
+{
+    color: navy;
+}
+.slide a:active,
+.slide a:hover
+{
+    color: red;
+    text-decoration: underline;
+}
+.slide a
+{
+    text-decoration: none;
+}
+ol,
+ul
+{
+    margin: 0.5em 1.5em 0.5em 1.5em;
+    padding: 0;
+}
+.slide ul
+{
+    list-style-type: square;
+}
+.slide ul ul ul
+{
+    list-style-type: circle;
+}
+.slide ul ul,
+.slide ul ul ul ul
+{
+    list-style-type: disc;
+}
+.slide li
+{
+    margin-top: 0.5em;
+}
+.slide li li
+{
+    font-size: 85%;
+    font-style: italic;
+}
+.slide li li li
+{
+    font-size: 85%;
+    font-style: normal;
+}
+.slide div dt
+{
+  margin-left: 0;
+  margin-top: 1em;
+  margin-bottom: 0.5em;
+  font-weight: bold;
+}
+.slide div dd
+{
+  margin-left: 2em;
+  margin-bottom: 0.5em;
+}
+.slide p,
+.slide pre,
+.slide ul,
+.slide ol,
+.slide blockquote,
+.slide h2,
+.slide h3,
+.slide h4,
+.slide h5,
+.slide h6,
+.slide dl,
+.slide table
+{
+    margin-left: 1em;
+    margin-right: 1em;
+}
+.slide-footer
+{
+    background-color: #EEE;
+    font-size: 20px;
+    height: 50px;
+    left: 0;
+    position: fixed;
+    right: 0;
+    width: 100%;
+    top: calc(100% - 50px);
+}
+.slide-footer x-button
+{
+    font-size: 14px;
+    padding: 5px;
+    margin: 2px;
+}
+.slide-footer .big-p
+{
+    font-size: 30px;
+    position: relative;
+    top: 3px;
+}
+@media print
+{
+    body,
+    #game-screen,
+    #game-content
+    {
+        display: unset;
+        height: fit-content;
+        position: unset;
+    }
+    div.slide
+    {
+        display: unset;
+        height: unset;
+        page-break-inside: avoid;
+        position: unset;
+    }
+    div.page-break
+    {
+        page-break-after: always;
+        visibility: hidden;
+    }
+    .slide-footer
+    {
+        display: none;
+    }
+}
+
+
+/*
+ * Styles related to a Save/Load Game Location
+ */
+.filled
+{
+    background-color: blue;
+    color: white;
+}
+table.save-table
+{
+    width: 7in;
+}
+.mobile table.save-table
+{
+    width: 320px;
+}
+table.save-table,
+table.save-table tr,
+table.save-table th,
+table.save-table td
+{
+    border-collapse: collapse;
+    border: 1px solid black;
+    padding:10px;
+    text-align: center;
+}
+.mobile table.save-table,
+.mobile table.save-table tr,
+.mobile table.save-table th,
+.mobile table.save-table td
+{
+    padding:3px;
+}
+table.save-table td.save-name
+{
+    width: 3in;
+}
+.mobile table.save-table td.save-name
+{
+    width: 100px;
+}
+.mobile #save-disk,
+.mobile #load-disk
+{
+    visibility: hidden;
+}
+/*
+ * Styles specific to how we want HTML to appear in the game.
+ * Also has styles for the new x- tags that we use for presentation
+ */
+h1
+{
+    margin: 0px;
+    padding: 0px;
+}
+img
+{
+    max-width: 90%;
+}
+figure
+{
+    max-width: 90%;
+}
+figure > img
+{
+    max-width: 100%;
+}
+input
+{
+    border: 2px solid lightblue;
+    border-radius: 5px;
+    font-size: 18pt;
+    padding: 2px;
+}
+a.disabled
+{
+    color: gray;
+    cursor: default;
+    pointer-events: none;
+}
+x-button
+{
+    background-color: #F0F0F6;
+    border: 1px solid gray;
+    border-radius: 5px;
+    color: black;
+    display: inline-block;
+    font-size: 18pt;
+    font-weight: bold;
+    padding: 8px;
+    margin: 3px;
+}
+x-button.disabled
+{
+    border: 1px solid lightgray;
+    background-color: #F6F6FA;
+    color: #666;
+    cursor: not-allowed;
+}
+x-button:hover
+{
+    background-color: lightgray;
+}
+
+x-button.disabled:hover
+{
+    color: #666;
+    background-color: #F6F6FA;
+}
+x-speaker
+{
+    border: 3px solid black;
+    border-radius: 10px;
+    display: block;
+    font-size:18pt;
+    margin: 10px;
+    min-height: 110px;
+    padding: 10px;
+    width: 90%;
+}
+.mobile x-speaker
+{
+    margin: 2px;
+    padding: 4px;
+    width: 82%;
+}
+x-speaker figure:first-of-type
+{
+    border-radius: 5px;
+    display: block;
+    float: left;
+    margin: 0;
+    width: 120px;
+}
+.rtl x-speaker figure:first-of-type
+{
+    float: right;
+}
+.mobile x-speaker figure:first-of-type
+{
+    width: 80px;
+}
+x-speaker figure:first-of-type > img
+{
+    border-radius: 10px;
+    display: block;
+    height: 100px;
+    margin: auto;
+    width: 100px;
+}
+.mobile x-speaker figure:first-of-type > img
+{
+    width: 70px;
+    height: 70px;
+}
diff --git a/src/css/slidy.css b/src/css/slidy.css
deleted file mode 100755
index b29aa6483..000000000
--- a/src/css/slidy.css
+++ /dev/null
@@ -1,402 +0,0 @@
-/* slidy.css
-
-   Copyright (c) 2005-2010 W3C (MIT, ERCIM, Keio), All Rights Reserved.
-   W3C liability, trademark, document use and software licensing
-   rules apply, see:
-
-   http://www.w3.org/Consortium/Legal/copyright-documents
-   http://www.w3.org/Consortium/Legal/copyright-software
-*/
-body
-{
-  margin: 0 0 0 0;
-  padding: 0 0 0 0;
-  width: 100%;
-  height: 100%;
-  color: black;
-  background-color: white;
-  font-family: "Gill Sans MT", "Gill Sans", GillSans, sans-serif;
-  font-size: 14pt;
-}
-
-div.toolbar {
-  position: fixed; z-index: 200;
-  top: auto; bottom: 0; left: 0; right: 0;
-  height: 1.2em; text-align: right;
-  padding-left: 1em;
-  padding-right: 1em;
-  font-size: 60%;
-  color: red;
-  background-color: rgb(240,240,240);
-  border-top: solid 1px rgb(180,180,180);
-}
-
-div.toolbar span.copyright {
-  color: black;
-  margin-left: 0.5em;
-}
-
-div.initial_prompt {
-  position: absolute;
-  z-index: 1000;
-  bottom: 1.2em;
-  width: 100%;
-  background-color: rgb(200,200,200);
-  opacity: 0.35;
-  background-color: rgba(200,200,200, 0.35);
-  cursor: pointer;
-}
-
-div.initial_prompt p.help {
-  text-align: center;
-}
-
-div.initial_prompt p.close {
-  text-align: right;
-  font-style: italic;
-}
-
-div.slidy_toc {
-  position: absolute;
-  z-index: 300;
-  width: 60%;
-  max-width: 30em;
-  height: 30em;
-  overflow: auto;
-  top: auto;
-  right: auto;
-  left: 4em;
-  bottom: 4em;
-  padding: 1em;
-  background: rgb(240,240,240);
-  border-style: solid;
-  border-width: 2px;
-  font-size: 60%;
-}
-
-div.slidy_toc .toc_heading {
-  text-align: center;
-  width: 100%;
-  margin: 0;
-  margin-bottom: 1em;
-  border-bottom-style: solid;
-  border-bottom-color: rgb(180,180,180);
-  border-bottom-width: 1px;
-}
-
-div.slide {
-  z-index: 20;
-  margin: 0 0 0 0;
-  padding-top: 0;
-  padding-bottom: 0;
-  padding-left: 20px;
-  padding-right: 20px;
-  border-width: 0;
-  clear: both;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  line-height: 120%;
-  background-color: transparent;
-}
-
-div.background {
-  display: none;
-}
-
-div.handout {
-  margin-left: 20px;
-  margin-right: 20px;
-}
-
-div.slide.titlepage {
-  text-align: center;
-}
-
-div.slide.titlepage h1 {
-  padding-top: 10%;
-  margin-right: 0;
-}
-
-div.slide h1 {
-  padding-left: 0;
-  padding-right: 20pt;
-  padding-top: 4pt;
-  padding-bottom: 4pt;
-  margin-top: 0;
-  margin-left: 0;
-  margin-right: 60pt;
-  margin-bottom: 0.5em;
-  display: block;
-  font-size: 160%;
-  line-height: 1.2em;
-  background: transparent;
-}
-
-@media screen and (max-device-width: 1024px)
-{
-  div.slide { font-size: 100%; }
-}
-
-@media screen and (max-device-width: 800px)
-{
-  div.slide { font-size: 200%; }
-  div.slidy_toc {
-    top: 1em;
-    left: 1em;
-    right: auto;
-    width: 80%;
-    font-size: 180%;
-  }
-}
-
-div.toc-heading {
-  width: 100%;
-  border-bottom: solid 1px rgb(180,180,180);
-  margin-bottom: 1em;
-  text-align: center;
-}
-
-img {
-  image-rendering: optimize-quality;
-}
-
-pre {
- font-size: 80%;
- font-weight: bold;
- line-height: 120%;
- padding-top: 0.2em;
- padding-bottom: 0.2em;
- padding-left: 1em;
- padding-right: 1em;
- border-style: solid;
- border-left-width: 1em;
- border-top-width: thin;
- border-right-width: thin;
- border-bottom-width: thin;
- border-color: #95ABD0;
- color: #00428C;
- background-color: #E4E5E7;
-}
-
-li pre { margin-left: 0; }
-
-blockquote { font-style: italic }
-
-img { background-color: transparent }
-
-p.copyright { font-size: smaller }
-
-.center { text-align: center }
-.footnote { font-size: smaller; margin-left: 2em; }
-
-a img { border-width: 0; border-style: none }
-
-a:visited { color: navy }
-a:link { color: navy }
-a:hover { color: red; text-decoration: underline }
-a:active { color: red; text-decoration: underline }
-
-a {text-decoration: none}
-.toolbar a:link {color: blue}
-.toolbar a:visited {color: blue}
-.toolbar a:active {color: red}
-.toolbar a:hover {color: red}
-
-ul { list-style-type: square; }
-ul ul { list-style-type: disc; }
-ul ul ul { list-style-type: circle; }
-ul ul ul ul { list-style-type: disc; }
-li { margin-left: 0.5em; margin-top: 0.5em; }
-li li { font-size: 85%; font-style: italic }
-li li li { font-size: 85%; font-style: normal }
-
-div dt
-{
-  margin-left: 0;
-  margin-top: 1em;
-  margin-bottom: 0.5em;
-  font-weight: bold;
-}
-div dd
-{
-  margin-left: 2em;
-  margin-bottom: 0.5em;
-}
-
-
-p,pre,ul,ol,blockquote,h2,h3,h4,h5,h6,dl,table {
-  margin-left: 1em;
-  margin-right: 1em;
-}
-
-p.subhead { font-weight: bold; margin-top: 2em; }
-
-.smaller { font-size: smaller }
-.bigger { font-size: 130% }
-
-td,th { padding: 0.2em }
-
-ul {
-  margin: 0.5em 1.5em 0.5em 1.5em;
-  padding: 0;
-}
-
-ol {
-  margin: 0.5em 1.5em 0.5em 1.5em;
-  padding: 0;
-}
-
-ul { list-style-type: square; }
-ul ul { list-style-type: disc; }
-ul ul ul { list-style-type: circle; }
-ul ul ul ul { list-style-type: disc; }
-
-ul li {
-  list-style: square;
-  margin: 0.1em 0em 0.6em 0;
-  padding: 0 0 0 0;
-  line-height: 140%;
-}
-
-ol li {
-  margin: 0.1em 0em 0.6em 1.5em;
-  padding: 0 0 0 0px;
-  line-height: 140%;
-  list-style-type: decimal;
-}
-
-li ul li {
-  font-size: 85%;
-  font-style: italic;
-  list-style-type: disc;
-  background: transparent;
-  padding: 0 0 0 0;
-}
-li li ul li {
-  font-size: 85%;
-  font-style: normal;
-  list-style-type: circle;
-  background: transparent;
-  padding: 0 0 0 0;
-}
-li li li ul li {
-  list-style-type: disc;
-  background: transparent;
-  padding: 0 0 0 0;
-}
-
-li ol li {
-  list-style-type: decimal;
-}
-
-
-li li ol li {
-  list-style-type: decimal;
-}
-
-/*
- setting class="outline on ol or ul makes it behave as an
- ouline list where blocklevel content in li elements is
- hidden by default and can be expanded or collapsed with
- mouse click. Set class="expand" on li to override default
-*/
-
-ol.outline li:hover { cursor: pointer }
-ol.outline li.nofold:hover { cursor: default }
-
-ul.outline li:hover { cursor: pointer }
-ul.outline li.nofold:hover { cursor: default }
-
-ol.outline { list-style:decimal; }
-ol.outline ol { list-style-type:lower-alpha }
-
-ol.outline li.nofold {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/nofold-dim.gif) no-repeat 0px 0.5em;
-}
-ol.outline li.unfolded {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/fold-dim.gif) no-repeat 0px 0.5em;
-}
-ol.outline li.folded {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/unfold-dim.gif) no-repeat 0px 0.5em;
-}
-ol.outline li.unfolded:hover {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/fold.gif) no-repeat 0px 0.5em;
-}
-ol.outline li.folded:hover {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/unfold.gif) no-repeat 0px 0.5em;
-}
-
-ul.outline li.nofold {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/nofold-dim.gif) no-repeat 0px 0.5em;
-}
-ul.outline li.unfolded {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/fold-dim.gif) no-repeat 0px 0.5em;
-}
-ul.outline li.folded {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/unfold-dim.gif) no-repeat 0px 0.5em;
-}
-ul.outline li.unfolded:hover {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/fold.gif) no-repeat 0px 0.5em;
-}
-ul.outline li.folded:hover {
-  padding: 0 0 0 20px;
-  background: transparent url(../graphics/unfold.gif) no-repeat 0px 0.5em;
-}
-
-/* for slides with class "title" in table of contents */
-a.titleslide { font-weight: bold; font-style: italic }
-
-/*
- hide images for work around for save as bug
- where browsers fail to save images used by CSS
-*/
-img.hidden { display: none; visibility: hidden }
-div.initial_prompt { display: none; visibility: hidden }
-
-  div.slide {
-     visibility: visible;
-     position: inherit;
-  }
-  div.handout {
-     border-top-style: solid;
-     border-top-width: thin;
-     border-top-color: black;
-  }
-
-@media screen {
-  .hidden { display: none; visibility: visible }
-
-  div.slide.hidden { display: block; visibility: visible }
-  div.handout.hidden { display: block; visibility: visible }
-  div.background { display: none; visibility: hidden }
-  body.single_slide div.initial_prompt { display: block; visibility: visible }
-  body.single_slide div.background { display: block; visibility: visible }
-  body.single_slide div.background.hidden { display: none; visibility: hidden }
-  body.single_slide .invisible { visibility: hidden }
-  body.single_slide .hidden { display: none; visibility: hidden }
-  body.single_slide div.slide { position: absolute }
-  body.single_slide div.handout { display: none; visibility: hidden }
-}
-
-@media print {
-  .hidden { display: block; visibility: visible }
-
-  div.slide pre { font-size: 60%; padding-left: 0.5em; }
-  div.toolbar { display: none; visibility: hidden; }
-  div.slidy_toc { display: none; visibility: hidden; }
-  div.background { display: none; visibility: hidden; }
-  div.slide { page-break-before: always }
-  /* :first-child isn't reliable for print media */
-  div.slide.first-slide { page-break-before: avoid }
-}
\ No newline at end of file
diff --git a/src/library/WikiParser.php b/src/library/WikiParser.php
index 72c8b41f9..8e8eafe70 100644
--- a/src/library/WikiParser.php
+++ b/src/library/WikiParser.php
@@ -511,14 +511,14 @@ class WikiParser implements CrawlConstants
             C\NS_LIB . "makeTableCallback", $document);
         if ($page_type == "presentation") {
             $lines = explode("\n", $document);
-            $out_document = "";
+            $out_document = "<x-slides>";
             $slide = "";
-            $div = "<div class='slide'>";
+            $div = "<x-slide>";
             foreach ($lines as $line) {
                 if (trim($line) == "....") {
                     $slide = $this->processRegexes($slide);
-                    $out_document .= $div .$this->cleanLinksAndParagraphs(
-                        $slide) ."</div>";
+                    $out_document .= $div . $this->cleanLinksAndParagraphs(
+                        $slide) ."</x-slide>";
                     $slide = "";
                 } else {
                     $slide .= $line."\n";
@@ -526,9 +526,10 @@ class WikiParser implements CrawlConstants
             }
             $last_slide = "";
             if (!empty(trim($slide))) {
-                $last_slide = $div . $this->processRegexes($slide) . "</div>";
+                $last_slide = $div . $this->processRegexes($slide) .
+                    "</x-slide>";
             }
-            $document = $out_document . $last_slide;
+            $document = $out_document . $last_slide . "</x-slides>";
         } else if ($handle_big_files) {
             $document= $this->processRegexes($document);
             $document = $this->cleanLinksAndParagraphs($document);
diff --git a/src/scripts/frise.js b/src/scripts/frise.js
new file mode 100644
index 000000000..228042566
--- /dev/null
+++ b/src/scripts/frise.js
@@ -0,0 +1,3156 @@
+/**
+ * FRISE (FRee Interactive Story Engine)
+ * A light-weight engine for writing interactive fiction and games.
+ *
+ * Copyright 2022-2024 Christopher Pollett chris@pollett.org
+ *
+ * @license
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * @file The JS code used to manage a FRISE game
+ * @author Chris Pollett
+ * @link https://www.frise.org/
+ * @copyright 2022 - 2024
+ */
+/*
+ * Global Variables
+ */
+/**
+ * Contains {locale => {to_translate_id => translation}} for each
+ * item in the game engine that needs to be localized.
+ * There are not too many strings to translate. For a particular game you
+ * could tack on to this tl variable after you load game.js in a script
+ * tag before you call initGame.
+ * @type {Object}
+ */
+var tl = {
+    "en": {
+        "init_slot_states_load" : "Load",
+        "init_slot_states_save" : "Save",
+        "restore_state_invalid_game" :
+            "Does not appear to be a valid game save",
+        "move_object_failed" : "moveObject(object_id, location) failed.\n" +
+            "Either the object or location does not exist.\n",
+        "slide_titles" : "Slide Titles"
+    }
+};
+/**
+ * Locale to use when looking up strings to be output in a particular language.
+ * We set the default here, but a particular game can override this in
+ * its script tag before calling initGame()
+ * @type {string}
+ */
+var locale = "en";
+/**
+ * Flag to set the text direction of the game to right-to-left.
+ * (if false, the text direction is left-to-right)
+ * @type {boolean}
+ */
+var is_right_to_left = false;
+/**
+ * Global game object used to track one interactive story fiction game
+ * @type {Game}
+ */
+var game;
+/**
+ * Flag used to tell if current user is interacting on a mobile device or not
+ * @type {boolean}
+ */
+var is_mobile = window.matchMedia("(max-width: 1000px)").matches;
+/**
+ * Number of x-include tags left to load before game can run
+ * @type {int}
+ */
+var includes_to_load = 0;
+/**
+ * Used as part of the closure of makeGameObject so that every game
+ * object has an integer id.
+ * @type {number}
+ */
+var object_counter = 0;
+/**
+ * Common Global Functions
+ */
+/**
+ * Returns an Element object from the current document which has the provided
+ * id.
+ * @param {string} id of element trying to get an object for
+ * @return {Element} desired element, if exists, else null
+ */
+function elt(id)
+{
+    return document.getElementById(id);
+}
+/**
+ * Returns a collection of objects for Element's that match the provided Element
+ * name in the current document.
+ *
+ * @param {string} name of HTML/XML Element wanted from current document
+ * @return {HTMLCollection} of matching Element's
+ */
+function tag(name)
+{
+    return document.getElementsByTagName(name);
+}
+/**
+ * Returns a list of Node's which match the CSS selector string provided.
+ *
+ * @param {string} a CSS selector string to match against current document
+ * @return {NodeList} of matching Element's
+ */
+function sel(selector)
+{
+    return document.querySelectorAll(selector);
+}
+/**
+ * Returns a game object based on the element in the current document of the
+ * provided id.
+ *
+ * @param {string} id of element in current document.
+ * @return {Object} a game object.
+ */
+function xelt(id)
+{
+    return makeGameObject(elt(id));
+}
+/**
+ * Returns an array of game objects based on the elements in the current
+ * document that match the CSS selector passed to it.
+ *
+ * @param {string} a CSS selector for objects in the current document
+ * @return {Array} of game objects based on the tags that matched the selector
+ */
+function xsel(selector)
+{
+    let tag_objects = sel(selector);
+    return makeGameObjects(tag_objects);
+}
+/**
+ * Returns an array of game objects based on the elements in the current
+ * document of the given tag name.
+ *
+ * @param {string} a name of a tag such as x-location or x-object.
+ * @return {Array} of game objects based on the tags that matched the selector
+ */
+function xtag(name)
+{
+    let tag_objects = tag(name);
+    return makeGameObjects(tag_objects);
+}
+/**
+ * Sleep time many milliseconds before continuing to execute whatever code was
+ * executing. This function returns a Promise so needs to be executed with await
+ * so that the code following the sleep statement will be used to resolve the
+ * Promise
+ *
+ * @param {number} time time in milliseconds to sleep for
+ * @return {Promise} a promise whose resolve callback is to be executed after
+ *  that many milliseconds.
+ */
+function sleep(time)
+{
+  return new Promise(resolve => setTimeout(resolve, time));
+}
+/**
+ * Adds a link with link text message which when clicked will allow the
+ * rendering of a Location to continue.
+ * @param {string} message link text for the link that is pausing the rendering
+ *  of the current location.
+ * @param {string} any html that should appear after the proceed link
+ * @return {Promise} which will be resolved which the link is clicked.
+ */
+function clickProceed(message, rest = "")
+{
+    let game_content = elt("game-content");
+    game.tick++;
+    game_content.innerHTML +=
+        `<a id='click-proceed${game.tick}' href=''>${message}</a>${rest}`;
+    return new Promise(resolve =>
+        elt(`click-proceed${game.tick}`).addEventListener("click", resolve));
+}
+
+/**
+ *  Given a rectangle within an Image object of a DollSlot doll_part
+ *  specified by a point x,y and a width and a height. Scales it to a
+ *  corresponding rectangle in the DollSlot coordinates (coordinates
+ *  used to specify where it is within a Doll).
+ *
+ *  @param {int} x coordinate of the image rectangle
+ *  @param {int} y coordinate of the image rectangle
+ *  @param {int} width of the image rectangle
+ *  @param {int} height of the image rectangle
+ *  @param {DollSlot} doll_part with respect to which the coordinate
+ *      transformation is done
+ *  @param {Array<int>} of length 4 containing output [x, y, width, height]
+ */
+ function imageCoordinates(x, y, width, height, doll_part)
+ {
+     let scale_x = doll_part.image.width/parseInt(doll_part.width);
+     let scale_y = doll_part.image.height/parseInt(doll_part.height);
+     let part_x = parseInt(doll_part.x ?? 0);
+     let part_y = parseInt(doll_part.y ?? 0);
+     let out_x = Math.floor(scale_x * (x - part_x));
+     let out_y = Math.floor(scale_y * (y - part_y));
+     let out_width = Math.floor(scale_x * width);
+     let out_height = Math.floor(scale_y * height);
+     return [out_x, out_y, out_width, out_height];
+ }
+
+/**
+ * Given an array of arrays [Object, property_name1, property_name2, ...]
+ * Parses the property names to int's in the given object. If the
+ * this results in Not a Number (NaN) then sets the property to the
+ * provided default value
+ *
+ * @param Array<Array<Object, String...>>
+ * @param int default_value
+
+ */
+function parseObjectsInts(objects_ints, default_value = 0)
+{
+    for (const object_ints of objects_ints) {
+        let obj = object_ints[0];
+        for (field_name of object_ints[1]) {
+            let convert = obj[field_name];
+            convert = parseInt(convert);
+            obj[field_name] = (isNaN(convert)) ? default_value : convert;
+        }
+    }
+}
+/**
+ * Creates from an HTMLCollection or Nodelist of x-object or x-location
+ * elements an array of Location's or Object's suitable for a FRISE Game
+ * object. Elements of the collection whose name aren't of the form x-
+ * or which don't have id's are ignored. Otherwise, the id field of
+ * the output Object or Location is set to the value of the x-object or
+ * x-location's id. Details about how a suitable FRISE Game object is created
+ * can be found @see makeGameObject()
+ *
+ * @param {HTMLCollection|Nodelist} tag_objects collection to be converted into
+ *  an array FRISE game objects or location objects.
+ * @return {Array} an array of FRISE game or location objects.
+ */
+function makeGameObjects(tag_objects)
+{
+    let game_objects = {};
+    for (const tag_object of tag_objects) {
+        let game_object = makeGameObject(tag_object);
+        if (game_object && game_object.id) {
+            game_objects[game_object.id] = game_object;
+        }
+    }
+    return game_objects;
+}
+/**
+ * Upper cases first letter of a string
+ * @param {string} str string to upper case the first letter of
+ * @return {string} result of upper-casing
+ */
+function upperFirst(str)
+{
+    if (!str) {
+        return "";
+    }
+    let upper_first = str.charAt(0).toUpperCase();
+    return upper_first + str.slice(1);
+}
+/**
+ * Given two rectangles: [x1, y1, width1, height1] and [x2, y2, width2, height2]
+ * Computes an intersection [x_out, y_out, width_out, height_out] of the
+ * rectangles if the rectangles intersect and outputs false otherwise.
+ *
+ * @param {Array<int>} rect1 4-tuple [x1, y1, width1, height1] of first
+ *  rectangle
+ * @param {Array<int>} rect2 4-tuple [x1, y1, width1, height1] of second
+ *  rectangle
+ * @return {Array<int>|boolean} 4-tuple [x_out, y_out, width_out, height_out] of
+ *  intersection rectangle or false otherwise
+ */
+function intersectRectangles(rect1, rect2)
+{
+    [x1, y1, width1, height1] = rect1.map((x) => {return 1*x}); //make int
+    [x2, y2, width2, height2] = rect2.map((x) => {return 1*x});
+    let x_out = Math.max(x1, x2);
+    let y_out = Math.max(y1, y2);
+    let width_out = Math.min(x1 + width1, x2 + width2) - x_out;
+    let height_out = Math.min(y1 + height1, y2 + height2) - y_out;
+    if (isNaN(x_out) || isNaN(y_out) || isNaN(width_out) ||
+        isNaN(height_out) || width_out <= 0 || height_out <= 0) {
+        return false;
+    }
+    return [x_out, y_out, width_out, height_out];
+}
+/**
+ * Used to convert a DOM Element dom_object to an Object or Location suitable
+ * for a FRISE game. dom_object's whose tagName does not begin with x-
+ * will result in null being returned. If the tagName is x-location, a
+ * Location object will be returned, otherwise, a Javascript Object is returned.
+ * The innerHTML of any subtag of an x-object or an
+ * x-location beginning with x- becomes the value of a field in the resulting
+ * object with the name of the tag less x-. For example, a DOM Object
+ * representing the following HTML code:
+ * <x-object id="bob">
+ *   <x-name>Robert Smith</x-name>
+ *   <x-age>25</x-age>
+ * </x-object>
+ * will be processed to a Javascript Object
+ * {
+ *   id: "bob",
+ *   name: "Robert Smith",
+ *   age: "25"
+ * }
+ * @param {Element} DOMElement to be convert into a FRISE game Object or
+ *  Location
+ * @return {Object} the resulting FRISE game Object or Location or
+ *  null if the tagName of the DOMElement didn't begin with x-
+ */
+function makeGameObject(dom_object)
+{
+    if (!dom_object || dom_object.tagName.substring(0, 2) != "X-") {
+        return null;
+    }
+    let tag_name = dom_object.tagName.slice(2);
+    let type = dom_object.getAttribute("type");
+    if (type) {
+        type = upperFirst(type);
+    } else if (tag_name && tag_name != "OBJECT") {
+        type = upperFirst(tag_name.toLowerCase());
+    } else {
+        type = "Object";
+    }
+    let game_object;
+    if (type == "Location") {
+        game_object = new Location();
+    } else if (type == "Doll") {
+        game_object = new Doll();
+    } else if (type == "Slots") {
+        type = "DollSlots";
+        game_object = new DollSlots();
+    } else if (type == "Slot") {
+        type = "DollSlot";
+        game_object = new DollSlot();
+    } else {
+        game_object = {};
+    }
+    if (dom_object.id) {
+        game_object.id = dom_object.id;
+    } else {
+        game_object.id = "oid" + object_counter;
+        object_counter++;
+    }
+    let style = dom_object.getAttribute('style');
+    if (typeof style != 'undefined') {
+        game_object.style = style;
+    }
+    let class_list = dom_object.getAttribute('class');
+    if (typeof class_list != 'undefined') {
+        game_object["class"] = class_list;
+    }
+    let has = {
+        'color' : false,
+        'icon' : false,
+        'height' : false,
+        'position' : false,
+        'present' : false,
+        'slots' : false,
+        'width' : false
+    };
+    let script_tags = {
+        "text/action" : "X-ACTION",
+        "text/default-action" : "X-DEFAULT-ACTION",
+        "text/click-listener" : "X-CLICK-LISTENER",
+        "text/collision-listener" : "X-COLLISION-LISTENER",
+        "text/update-listener" : "X-UPDATE-LISTENER",
+    };
+    for (const child of dom_object.children) {
+        let tag_name = child.tagName;
+        if (tag_name == 'SCRIPT') {
+            let script_type = child.getAttribute("type");
+            if (typeof script_tags[script_type] != 'undefined') {
+                tag_name = script_tags[script_type];
+            }
+        }
+        if (tag_name.substring(0, 2) != "X-") {
+            continue;
+        }
+        let attribute_name = tag_name.slice(2);
+        if (attribute_name) {
+            attribute_name = attribute_name.toLowerCase();
+            has[attribute_name] = true;
+            if (attribute_name == 'present') {
+                if (!game_object[attribute_name]) {
+                    game_object[attribute_name] = [];
+                }
+                let check = "";
+                let is_else = false;
+                for(let check_attr of ['ck', 'check', 'else-ck',
+                    'else-check', 'else']) {
+                    let tmp_check = child.getAttribute(check_attr);
+                    if (tmp_check) {
+                        check = tmp_check;
+                        if (['else-ck', 'else-check'].includes(check_attr)) {
+                            is_else = true;
+                        }
+                        break;
+                    }
+                }
+                let stage = child.getAttribute("stage");
+                if (!stage) {
+                    stage = "";
+                }
+                game_object[attribute_name].push([check, stage, is_else,
+                    child.innerHTML]);
+            } else if (type == 'Doll' && attribute_name == 'slots') {
+                game_object[attribute_name] = makeGameObject(child);
+            } else if (type == 'DollSlots' && attribute_name == 'slot') {
+                let slot = makeGameObject(child);
+                if (slot.type == 'DollSlot') {
+                    game_object.push(slot);
+                }
+            } else {
+                game_object[attribute_name] = child.innerHTML;
+            }
+        }
+    }
+    game_object.type = type;
+    if (type == 'Location') {
+        game_object.has_present = has['present'];
+    } else if (type == 'Object') {
+        game_object.has_position = has['position'];
+    } else if (type == 'Doll') {
+        game_object.has_height = has['height'];
+        game_object.has_icon = has['icon'];
+        game_object.has_color = has['color'];
+        game_object.has_width = has['width'];
+        if (!has['slots']) {
+            game_object.slots = new DollSlots();
+        }
+        game_object.init();
+    }
+    return game_object;
+}
+/**
+ * Used to change the display css property of the element of the provided id
+ * to display_type if it doesn't already have that value, if it does,
+ * then the display property is set to none. If display_type is not provided
+ * then its value is assumed to be block.
+ *
+ * @param {string} id of HTML Element to toggle display property of
+ * @param {string} display_type value to toggle between it and none.
+ */
+function toggleDisplay(id, display_type = "block")
+{
+    let obj = elt(id);
+    if (obj.style.display == display_type) {
+        value = "none";
+    } else {
+        value = display_type;
+    }
+    obj.style.display = value;
+    if (value == "none") {
+        obj.setAttribute('aria-hidden', true);
+    } else {
+        obj.setAttribute('aria-hidden', false);
+    }
+}
+/**
+ * Used to toggle the display or non-display of the main navigation bar
+ * on the side of game screen
+ */
+function toggleMainNav()
+{
+    let game_content = elt('game-content');
+    let nav_obj = elt('main-nav');
+    if ((!nav_obj.style.left && !nav_obj.style.right) ||
+        nav_obj.style.left == '0px' || nav_obj.style.right == '0px') {
+        game_content.style.width = "calc(100% - 40px)";
+        if (is_right_to_left) {
+            game_content.style.right = "55px";
+            nav_obj.style.right = '-300px';
+        } else {
+            game_content.style.left = "55px";
+            nav_obj.style.left = '-300px';
+        }
+        if (is_mobile) {
+            nav_obj.style.width = "240px";
+            game_content.style.width = "calc(100% - 70px)";
+        }
+        nav_obj.style.backgroundColor = 'white';
+    } else {
+        if (is_right_to_left) {
+            nav_obj.style.right = '0px';
+            game_content.style.right = "300px";
+        } else {
+            nav_obj.style.left = '0px';
+            game_content.style.left = "300px";
+        }
+        game_content.style.width = "calc(100% - 480px)";
+        if (is_mobile) {
+            nav_obj.style.width = "100%";
+            if (is_right_to_left) {
+                game_content.style.right = "100%";
+            } else {
+                game_content.style.left = "100%";
+            }
+        }
+        nav_obj.style.backgroundColor = 'lightgray';
+    }
+}
+/**
+ * Adds click event listeners to all anchor objects in a list
+ * that have href targets beginning with #. Such a target
+ * is to a location within the current game, so the click event callback
+ * then calls game.takeTurn based on this url.
+ *
+ * @param {NodeList|HTMLCollection} anchors to add click listeners for
+ *  game take turn callbacks.
+ */
+function addListenersAnchors(anchors)
+{
+    let call_toggle = false;
+    if (arguments.length > 1) {
+        if (arguments[1]) {
+            call_toggle = true;
+        }
+    }
+    for (const anchor of anchors) {
+        let url = anchor.getAttribute("href");
+        if (url && url[0] == "#") {
+            let hash = url;
+            anchor.innerHTML = "<span tabindex='0'>" +anchor.innerHTML +
+                "</span>";
+            let handle =  async (event) => {
+                let transition = anchor.getAttribute('data-transition');
+                let duration = anchor.getAttribute('data-duration');
+                let iterations = anchor.getAttribute('data-iterations');
+                let origin = anchor.getAttribute('data-transform-origin');
+                let preset_origins = { 'top-left-origin' : "0 0",
+                    'top-mid-origin' : "50% 0",
+                    'top-right-origin' : "100% 0",
+                    'bottom-left-origin' : "0 50%",
+                    'bottom-mid-origin' : "50% 100%",
+                    'bottom-right-origin' : "100% 100%",
+                    'mid-left-origin' : "0 50%",
+                    'mid-right-origin' : "100% 50%",
+                    'mid-mid-origin' : "50% 50%",
+                    'center-origin' : "50% 50%"
+                };
+                if (transition) {
+                    let game_content = elt('game-content');
+                    game_content.classList.add(transition);
+                    duration = (duration) ? parseInt(duration) : 2000;
+                    iterations = (iterations) ? parseInt(iterations) : 1;
+                    origin ??= "50% 50%";
+                    if (typeof preset_origins[origin] != 'undefined') {
+                        origin = preset_origins[origin];
+                    }
+                    game_content.style.animationDuration = duration + "ms";
+                    game_content.style.animationIterationCount = iterations;
+                    game_content.style.transformOrigin = origin;
+                    await sleep(duration);
+                    elt('game-content').classList.remove(transition);
+                }
+                let is_slides = sel("x-slides")[0] ? true : false;
+                if (is_slides) {
+                    window.location = hash;
+                    game.takeTurn(hash);
+                    return;
+                }
+                if (!anchor.classList.contains('disabled')) {
+                    game.takeTurn(hash);
+                    if (game.has_nav_bar && call_toggle) {
+                        toggleMainNav();
+                    }
+                }
+                event.preventDefault();
+                if (window.location.hash) {
+                    delete window.location.hash;
+                }
+            };
+            anchor.addEventListener('keydown', (event) => {
+                if (event.code == 'Enter' || event.code == 'Space') {
+                    handle(event);
+                }
+            });
+            anchor.addEventListener('click', (event) => handle(event));
+        } else if (url) {
+            anchor.innerHTML = "<span tabindex='0'>" +anchor.innerHTML +
+                "</span>";
+            anchor.addEventListener('click', (event) => {
+                window.location = url;
+            });
+        }
+    }
+}
+/**
+ * Used to disable any volatile links which might cause
+ * issues if clicked during rendering of the staging
+ * portion of a presentation. For example, saves screen links
+ * in either the main-nav panel/main-nav bar or in the
+ * game content area. The current place where this is used
+ * is at the start of rendering a location. A location
+ * might have several x-present tags, some of which could
+ * be drawn after a clickProceed, or a delay. As these
+ * steps in rendering are not good "save places", at the
+ * start of rendering a location, all save are disabled.
+ * Only after the last drawable x-present for a location
+ * has completed are they renabled.
+ */
+function disableVolatileLinks()
+{
+    for (let location_id of game.volatileLinks()) {
+        let volatile_links = sel(`[href~="#${location_id}"]`);
+        for (const volatile_link of volatile_links) {
+            volatile_link.classList.add('disabled');
+        }
+    }
+}
+/**
+ * Used to re-enable any disabled volatile links
+ * in either the main-nav panel/main-nav bar or in the
+ * game content area. The current place where this is used
+ * is at the end of rendering a location. For why this is called,
+ * @see disableVolatileLinks()
+ */
+function enableVolatileLinks()
+{
+    for (let location_id of game.volatileLinks()) {
+        let volatile_links = sel(`[href~="#${location_id}"]`);
+        for (const volatile_link of volatile_links) {
+            volatile_link.classList.remove('disabled');
+        }
+    }
+}
+/**
+ * Given a string which may contain Javascript string interpolation expressions,
+ * i.e., ${some_expression}, produces a string where those expressions have
+ * been replaced with their values.
+ *
+ * @param {string} text to have Javascript interpolation expressions replaced
+ *  with their values
+ * @return {string} result of carrying out the replacement
+ */
+function interpolateVariables(text)
+{
+    old_text = "";
+    while (old_text != text) {
+        old_text = text;
+        let interpolate_matches = text.match(/\$\{([^\}]+)\}/);
+        if (interpolate_matches && interpolate_matches[1]) {
+            let interpolate_var = interpolate_matches[1];
+            if (interpolate_var.replace(/\s+/, "") != "") {
+                interpolate_var = interpolate_var.replaceAll(/\&gt\;?/g, ">");
+                interpolate_var = interpolate_var.replaceAll(/\&lt\;?/g, "<");
+                interpolate_var = interpolate_var.replaceAll(/\&amp\;?/g, "&");
+                if (interpolate_var) {
+                    let interpolate_value = eval(interpolate_var);
+                    text = text.replace(/\$\{([^\}]+)\}/, interpolate_value);
+                }
+            }
+        }
+    }
+    return text;
+}
+/**
+ * A data-ck attribute on any tag other than an x-present tag can
+ * contain a Javascript boolean expression to control the display
+ * or non-display of that element. This is similar to a ck attribute
+ * of an x-present tag. This method evalutes data-ck expressions for
+ * each tag in the text in its section argument and adds a class="none"
+ * attribute to that tag if it evaluates to false (causing it not to
+ * display). The string after these substitutions is returned.
+ *
+ * @param {string} section of text to check for data-ck attributes and
+ *  for which to carry out the above described substitutions
+ * @return {string} after substitutions have been carried out.
+ */
+function evaluateDataChecks(section)
+{
+    let quote = `(?:(?:'([^']*)')|(?:"([^"]*)"))`;
+    let old_section = "";
+    while (section != old_section) {
+        old_section = section;
+        let data_ck_pattern = new RegExp(
+            "\<(?:[^\>]+)((?:data\-)?ck\s*\=\s*(" + quote + "))(?:[^\>]*)\>",
+        'i');
+        section = section.replace(/\&amp;/g, "&");
+        let data_ck_match = section.match(data_ck_pattern);
+        if (!data_ck_match || section.includes("cli" + data_ck_match[1])) {
+            //don't want above to accidentally also match onclick=
+            continue;
+        }
+        let condition = (typeof data_ck_match[3] == 'string') ?
+             data_ck_match[3] : data_ck_match[4];
+        if (typeof condition == 'string') {
+            let check_result = (condition.replace(/\s+/, "") != "") ?
+                eval(condition) : true;
+            if (check_result) {
+                section = section.replace(data_ck_match[1], " ");
+            } else {
+                section = section.replace(data_ck_match[1],
+                    " class='none' ");
+            }
+        }
+    }
+    return section;
+}
+/**
+ * Returns the game object with the provided id.
+ * @param {string} object_id to get game object for
+ * @return {object} game object associated with object_id if it exists
+ */
+function obj(object_id)
+{
+    return game.objects[object_id];
+}
+/**
+* Returns the game location with the provided id.
+* @param {string} object_id to get game Location for
+* @return {Location} game location associated with object_id if it exists
+ */
+function loc(location_id)
+{
+    return game.locations[location_id];
+}
+/**
+* Returns the game doll (paperdoll) with the provided id.
+* @param {string} object_id to get game doll for
+* @return {Location} game doll associated with object_id if it exists
+ */
+function doll(doll_id)
+{
+    return game.dolls[doll_id];
+}
+/**
+ * Returns the game object associated with the main-character. This
+ * function is just an abbreviation for obj('main-character')
+ * @return {object} game object associated with main-character
+ */
+function mc()
+{
+    return game.objects['main-character'];
+}
+/**
+ * Returns the location object associated with the main-character current
+ * position.
+ * @return {Location} associated with main-character position
+ */
+function here()
+{
+    return game.locations[game.objects['main-character'].position];
+}
+/**
+ * Returns the Location object the player begins the game at
+ */
+function baseLoc()
+{
+    return game.locations[game.base_location];
+}
+/**
+ * For use in default actions only!!! Returns whether the main-character is
+ * in the Location of the default action. In all other cases returns false
+ * @return {boolean} where the main-character is in the room of the current
+ *  default action
+ */
+function isHere()
+{
+    return game['is_here'];
+}
+/**
+ * Returns whether the main character has ever been to the location
+ * given by location_id
+ *
+ * @param {string} location_id id of location checking if main character has
+ *  been to
+ * @return {boolean} whther the main chracter has been there
+ */
+function hasVisited(location_id)
+{
+    return (loc(location_id).visited > 0);
+}
+/**
+ * Encapsulates one place that objects can be in a Game.
+ */
+class Location
+{
+    /**
+     * An array of [check_condition, staging, is_else, text_to_present] tuples
+     * typically coming from the x-present-tag's in the HTML of a Location.
+     * @type {Array}
+     */
+    present = [];
+    /**
+     * Number of times main-character has visited a location
+     * @type {int}
+     */
+    visited = 0;
+    /**
+     * Used to display a description of a location to the game content
+     * area. This description is based on the x-present tags that were
+     * in the x-location tag from which the Location was parse. For
+     * each such x-present tag in the order it was in the original HTML,
+     * the ck/else-ck condition and staging is first evaluated
+     * once/if the condition is satisfied, staging is processed (this may
+     * contain a delay or a clickProceed call), then the HTML contents of the
+     * tag are shown. In the case where the ck or else-ck evaluates to false
+     * than the x-present tag's contents are omitted. In addition to the
+     * usual HTML tags, an x-present tag can have x-speaker subtags. These
+     * allow one to present text from a speaker in bubbles. An x-present tag
+     * may also involve input tags to receive/update values for x-objects or
+     * x-locations.
+     */
+    async renderPresentation()
+    {
+        disableVolatileLinks();
+        let game_content = elt("game-content");
+        game_content.innerHTML = "";
+        if (typeof this["style"] != 'undefined' && this["style"]) {
+            game_content.setAttribute('style', this["style"]);
+        }
+        if (typeof this["class"] != 'undefined' && this["class"]) {
+            game_content.setAttribute('class', this["class"]);
+        }
+        game_content.scrollTop = 0;
+        game_content.scrollLeft = 0;
+        let check, staging, is_else, section_html;
+        let check_result, proceed, pause;
+        check_result = false;
+        for (let section of this.present) {
+            if (!section[3]) {
+                continue;
+            }
+            [check, staging, is_else, section_html] = section;
+            if (is_else && check_result) {
+                continue;
+            }
+            [check_result, proceed, pause] =
+                this.evaluateCheckConditionStaging(check, staging);
+            if (check_result) {
+                let prepared_section = this.prepareSection(section_html);
+                if (proceed) {
+                    let old_inner_html = game_content.innerHTML;
+                    event = await clickProceed(proceed);
+                    event.preventDefault();
+                    game_content.innerHTML = old_inner_html;
+                } else if (pause) {
+                    await sleep(pause);
+                }
+                /*
+                   A given prepared section might involve
+                   <x-stage></x-stage> tags to simulate a typewriter effect,
+                   or delays or clicks to continue.
+                   The code below is used to present these tags.
+                 */
+                let rest_section = prepared_section;
+                let final_section = "";
+                let xstage_open = "<x-stage";
+                let xstage_close = "</x-stage";
+                let open_pos, end_pos, greater_pos;
+                end_pos = rest_section.lastIndexOf(xstage_close);
+                if (end_pos > 0) {
+                    final_section = rest_section.substring(end_pos +
+                        xstage_close.length);
+                    greater_pos = final_section.search(">");
+                    if (greater_pos >= 0) {
+                        final_section = final_section.substring(greater_pos + 1);
+                    } else {
+                        greater_pos = final_section.length;
+                    }
+                    rest_section = rest_section.substring(0, end_pos +
+                        xstage_close.length + greater_pos);
+                }
+                let inner_html = game_content.innerHTML;
+                while ((open_pos = rest_section.search(xstage_open)) != -1) {
+                    let animation_speed = 50;
+                    let click_matches = false;
+                    let is_short_tag = false;
+                    inner_html += rest_section.substring(0, open_pos);
+                    rest_section = rest_section.substring(open_pos +
+                        xstage_open.length);
+                    greater_pos = rest_section.search(">");
+                    if (rest_section.charAt(greater_pos - 1) == "/") {
+                        is_short_tag = true;
+                    }
+                    if (greater_pos >= 0) {
+                        let tag_contents = rest_section.substring(0,
+                            greater_pos + 1);
+                        let delay_matches = tag_contents.match(
+                            /delay\=(\"(\d+)\"|\'(\d+)\')/);
+                        if (delay_matches) {
+                             animation_speed =
+                                 (typeof delay_matches[2] == 'undefined') ?
+                                 delay_matches[3] : delay_matches[2];
+                             animation_speed = parseInt(animation_speed);
+                        }
+                        click_matches = tag_contents.match(
+                            /delay\=(\"click\"|\'click\')/);
+                        rest_section = rest_section.substring(greater_pos + 1);
+                    } else {
+                        rest_section = "";
+                    }
+                    end_pos = rest_section.search(xstage_close);
+                    open_pos = rest_section.search(xstage_open);
+                    let stage_text = "";
+                    if (is_short_tag || end_pos == -1 || (open_pos > 0 &&
+                        end_pos > open_pos)) {
+                        end_pos = 0;
+                        // assume tag was not for delayed typing, just for delay
+                    } else {
+                        stage_text = rest_section.substring(0, end_pos);
+                        rest_section = rest_section.substring(end_pos +
+                            xstage_close.length);
+                        greater_pos = rest_section.search(">");
+                        if (greater_pos >= 0) {
+                            rest_section =
+                                rest_section.substring(greater_pos + 1);
+                        }
+                    }
+                    if (click_matches) {
+                        game_content.innerHTML = inner_html;
+                        event = await clickProceed(stage_text);
+                        event.preventDefault();
+                        event.stopPropagation();
+                        game_content.innerHTML = inner_html;
+                    } else {
+                        let old_length = inner_html.length;
+                        inner_html += stage_text;
+                        game.stop_stage = false;
+                        let game_content = elt('game-content');
+                        game_content.setAttribute('tabindex', 0);
+                        game_content.addEventListener('click', () => {
+                            game.stop_stage = true;
+                        });
+                        game_content.addEventListener('keydown', () => {
+                            game.stop_stage = true;
+                        });
+                        await sleep(animation_speed);
+                        for (let i = old_length; i <= old_length + end_pos;
+                            i++) {
+                            game_content.innerHTML = inner_html.substring(0, i)
+                            await sleep(animation_speed);
+                            if (game.stop_stage) {
+                                game_content.onclick = null;
+                                break;
+                            }
+                        }
+                    }
+                }
+                game_content.innerHTML = inner_html + rest_section +
+                    final_section;
+            }
+        }
+        // footer used so a game location page doesn't get squished
+        if (!sel("x-slides")[0]) {
+            game_content.innerHTML += "<div class='footer-space'></div>";
+        }
+        this.prepareControls();
+        if (this['screen-footer']) {
+            let footer = elt('screen-footer');
+            if (!footer) {
+                footer = document.createElement('div');
+                footer.id = 'screen-footer';
+                elt('game-screen').appendChild(footer);
+            }
+            footer.innerHTML = this['screen-footer'];
+        }
+        let anchors = sel("#game-screen a, #game-screen x-button");
+        addListenersAnchors(anchors);
+        this.renderDollHouses();
+        if (typeof sessionStorage['show_footer'] != 'undefined') {
+            if (!sessionStorage['show_footer'] ||
+                sessionStorage['show_footer'] == 'false') {
+                let slide_footer =
+                    sel('#game-screen .slide-footer')[0];
+                slide_footer.style.display = 'none';
+            }
+        }
+        if (!game.volatileLinks().includes(mc().position)) {
+            enableVolatileLinks();
+        }
+    }
+    /**
+     * For each Dollhouse in the current main-nav and game-content
+     * areas of the HTML browser window draw the Dollhouses contents to a
+     * canvas tag  associated with it. Then sets up any key, click, and
+     * collision  listeners associated with the Dollhouse to handle events
+     * on that canvas.
+     */
+    renderDollHouses()
+    {
+        for (const screen_id of ["main-nav", "game-content"]) {
+            let screen_part = elt(screen_id);
+            if (!screen_part) {
+                continue;
+            }
+            let part_width = screen_part.clientWidth;
+            let part_height = screen_part.clientHeight;
+            let dom_dollhouses = sel(`#${screen_id} x-dollhouse`);
+            for (const dom_dollhouse of dom_dollhouses) {
+                let dollhouse = makeGameObject(dom_dollhouse);
+                if (!dollhouse['doll-id']) {
+                    continue;
+                }
+                let doll_obj = doll(dollhouse['doll-id']);
+                if (!doll_obj) {
+                    continue;
+                }
+                let width = dollhouse.width ?? part_width;
+                let height = dollhouse.height ?? part_height;
+                let source_x = dollhouse['source-x'] ?? (doll_obj['source-x'] ??
+                    0);
+                dollhouse['source-x'] = source_x;
+                let source_y = dollhouse['source-y'] ?? (doll_obj['source-y'] ??
+                    0);
+                dollhouse['source-y'] = source_y;
+                let source_width = dollhouse['source-width'] ??
+                    (doll_obj['source-width'] ?? doll_obj.width);
+                dollhouse['source-width'] = source_width;
+                let source_height = dollhouse['source-height'] ??
+                    (doll_obj['source-height'] ?? doll_obj.height);
+                dollhouse['source-height'] = source_height;
+                let canvas = document.createElement('canvas');
+                let dom_class = dom_dollhouse.getAttribute('class');
+                dom_class = (dom_class) ? `block ${dom_class}` : 'block';
+                let dom_style = dom_dollhouse.getAttribute('style');
+                dom_style ??= '';
+                canvas.setAttribute('class', dom_class);
+                canvas.setAttribute('style', dom_style);
+                canvas.setAttribute('width', width);
+                canvas.setAttribute('height', height);
+                canvas.setAttribute('data-source-x', source_x);
+                canvas.setAttribute('data-source-y', source_y);
+                canvas.setAttribute('data-source-width', source_width);
+                canvas.setAttribute('data-source-height', source_height);
+                let is_navigable = (typeof dollhouse.navigable != 'undefined' &&
+                    (dollhouse.navigable === true ||
+                    dollhouse.navigable === "true"));
+                if (is_navigable) {
+                    let player_slot = doll_obj.getPlayerSlot(dollhouse);
+                    player_slot.init(doll_obj);
+                    dollhouse.player_slot = player_slot;
+                }
+                dollhouse.doll = doll_obj;
+                dollhouse.canvas = canvas;
+                dom_dollhouse.parentNode.insertBefore(canvas,
+                    dom_dollhouse.nextSibling);
+                doll_obj.render(canvas);
+                if (is_navigable ||
+                    doll_obj.has_collision_listeners ||
+                    doll_obj.has_click_listeners ||
+                    doll_obj.has_update_listeners) {
+                    this.addDollHouseListeners(dollhouse);
+                }
+            }
+        }
+    }
+    /**
+     * Sets up the canvas element listeners associated with a Dollhouse.
+     * Possible
+     *
+     * @param {Dollhouse} whose canvas will add listeners to
+     */
+    addDollHouseListeners(dollhouse)
+    {
+        let canvas = dollhouse.canvas;
+        let doll_obj = dollhouse.doll;
+        let player_slot = doll_obj.getPlayerSlot(dollhouse);
+        dollhouse.player_slot = player_slot;
+        if (dollhouse.initialized) {
+            return;
+        }
+        if ((dollhouse.navigable === true ||
+            dollhouse.navigable === "true")) {
+            parseObjectsInts([ [player_slot, ["x", "y", "width", "height"]],
+                [doll_obj, ["width", "height"]], [dollhouse, ["source-x",
+                "source-y", "source-width", "source-height",
+                'padding-top', 'padding-left', 'padding-bottom',
+                'padding-right']] ]);
+            dollhouse['source-width'] = (dollhouse['source-width'] > 0) ?
+                dollhouse['source-width'] : doll_obj.width;
+            dollhouse['source-height'] = (dollhouse['source-height'] > 0) ?
+                dollhouse['source-height'] : doll_obj.height;
+            let player_half_width = Math.floor(player_slot.width/2);
+            let player_half_height = Math.floor(player_slot.height/2);
+            let step_x = dollhouse['player-step-x'] ??
+                (dollhouse['player-step'] ?? 1);
+            step_x = parseInt(step_x);
+            let step_y = dollhouse['player-step-y'] ??
+                (dollhouse['player-step'] ?? 1);
+            step_y = parseInt(step_y);
+            let scroll_step_x = 0;
+            let scroll_step_y = 0;
+            let x_min = dollhouse['source-x'] + player_half_width;
+            let y_min = dollhouse['source-y'] + player_half_height;
+            let x_max = dollhouse['source-x'] + dollhouse['source-width'] -
+                player_half_width;
+            let y_max = dollhouse['source-y'] + dollhouse['source-height'] -
+                player_half_height;
+            if (dollhouse['scrollable'] && dollhouse['scrollable'] == "true") {
+                scroll_step_x = step_x;
+                scroll_step_y = step_y;
+                x_min = player_half_width;
+                x_max = doll_obj.width - player_half_width;
+                y_min = player_half_height;
+                y_max = doll_obj.height - player_half_height;
+            }
+            x_min += dollhouse['padding-left'];
+            x_max -= dollhouse['padding-right'];
+            y_min += dollhouse['padding-top'];
+            y_max -= dollhouse['padding-bottom'];
+            canvas.setAttribute('tabindex', 0);
+            canvas.focus();
+            canvas.addEventListener('keydown', event => {
+                if (player_slot.old_x &&
+                    dollhouse.has_collision_listeners) {
+                    return;
+                }
+                let change = false;
+                let old_values = [player_slot.x, player_slot.y,
+                    dollhouse['source-x'], dollhouse['source-y'],
+                    player_slot.width, player_slot.height
+                ];
+                let x_pos = player_slot.x + player_half_width;
+                let y_pos = player_slot.y + player_half_height;
+                switch(event.code) {
+                    case 'KeyW':
+                    case 'ArrowUp':
+                        if (y_pos > y_min) {
+                            change = true;
+                            dollhouse['source-y'] -= scroll_step_y;
+                            player_slot.y -= step_y;
+                        }
+                        break;
+                    case 'KeyS':
+                    case 'ArrowDown':
+                        if (y_pos < y_max) {
+                            change = true;
+                            dollhouse['source-y'] += scroll_step_y;
+                            player_slot.y += step_y;
+                        }
+                        break;
+                    case 'KeyA':
+                    case 'ArrowLeft':
+                        if (x_pos > x_min) {
+                            change = true;
+                            dollhouse['source-x'] -= scroll_step_x;
+                            player_slot.x -= step_x;
+                        }
+                        break;
+                    case 'KeyD':
+                    case 'ArrowRight':
+                        if (x_pos < x_max) {
+                            change = true;
+                            dollhouse['source-x'] += scroll_step_x;
+                            player_slot.x += step_x;
+                        }
+                        break;
+                }
+                if (change) {
+                    [player_slot.old_x, player_slot.old_y,
+                    player_slot.old_source_x, player_slot.old_source_y,
+                    player_slot.old_width, player_slot.old_height] =
+                        old_values;
+                }
+            });
+        }
+        if (doll_obj.has_click_listeners) {
+            canvas.addEventListener("click", (event) => {
+                if (doll_obj['click-listener']) {
+                    eval(doll_obj['click-listener']);
+                }
+                if (doll_obj.slots.has_click_listeners) {
+                    let rect = canvas.getBoundingClientRect();
+                    let x = event.clientX - rect.left;
+                    let y = event.clientY - rect.top;
+                    let source_x = dollhouse['source-x'] ?? 0;
+                    let source_y = dollhouse['source-y'] ?? 0;
+                    let source_width = dollhouse['source-width'] ??
+                        doll_obj.width;
+                    let source_height = dollhouse['source-height'] ??
+                        doll_obj.height;
+                    x = Math.floor((source_width * x / canvas.width) + source_x);
+                    y = Math.floor((source_height * y / canvas.height) +
+                        source_y);
+                    for (const slot of doll_obj.slots.items) {
+                        if (typeof slot != 'undefined' &&
+                            slot['click-listener']) {
+                            parseObjectsInts([[slot,
+                                ["x", "y", "width", "height"]]]);
+                            if (slot.x <= x && x <= slot.x + slot.width &&
+                                slot.y <= y && y <= slot.y + slot.height) {
+                                eval(slot['click-listener']);
+                                doll_obj.redraw = true;
+                            }
+                        }
+                    }
+                }
+            });
+        }
+        if (dollhouse['navigable'] || doll_obj.has_update_listeners ||
+            doll_obj.has_collision_listeners ||
+            doll_obj.has_click_listeners) {
+            let refresh = (dollhouse.refresh) ? parseInt(dollhouse.refresh):
+                game.dollhouse_refresh_rate;
+            game.dollhouse_update_ids[game.dollhouse_update_ids.length] =
+                setInterval((event) => {
+                    if (doll_obj['update-listener']) {
+                        eval(doll_obj['update-listener']);
+                        doll_obj.redraw = true;
+                    }
+                    for (const slot of doll_obj.slots.items) {
+                        if (typeof slot != 'undefined' &&
+                            slot['update-listener']) {
+                            eval(slot['update-listener']);
+                            doll_obj.redraw = true;
+                        }
+                    }
+                    if (player_slot && player_slot.old_x) {
+                        doll_obj.redraw = true;
+                    }
+                    if (doll_obj.has_collision_listeners) {
+                        doll_obj.checkCollisions(dollhouse);
+                    }
+                    if (player_slot && player_slot.old_x) {
+                        delete player_slot.old_x;
+                        delete player_slot.old_y;
+                        delete player_slot.old_source_x;
+                        delete player_slot.old_source_y;
+                        delete player_slot.old_width;
+                        delete player_slot.old_height;
+                    }
+                    if (doll_obj.redraw) {
+                        let source_x = dollhouse['source-x'] ?? 0;
+                        canvas.setAttribute('data-source-x', source_x);
+                        let source_y = dollhouse['source-y'] ?? 0;
+                        canvas.setAttribute('data-source-y', source_y);
+                        doll_obj.render(canvas);
+                        doll_obj.redraw = false;
+                    }
+                }, refresh);
+        }
+        dollhouse.initialized = true;
+    }
+    /**
+     * Prepares input, textareas, and select tags in the game so that they can
+     * bind to game Object or Location fields by adding various Javascript
+     * Event handlers. An input tag like
+     * <input data-for="bob" name="name" >
+     * binds with the name field of the bob game object (i.e., obj(bob).name).
+     * In the case above, as the default type of an input tag is text, this
+     * would produce a text field whose initial value is the current value
+     * obj(bob).name. If the user changes the field, the value of the obj
+     * changes with it. This set up binds input tags regardless of type, so it
+     * can be used with other types such as range, email, color, etc.
+     */
+    prepareControls()
+    {
+        const content_areas = ["main-nav", "game-content"];
+        for (const content_area of content_areas) {
+            let content = elt(content_area);
+            if (!content) {
+                continue;
+            }
+            if (content_area == 'main-nav') {
+                if (!content.hasOwnProperty("originalHTML")) {
+                    content.originalHTML = content.innerHTML;
+                }
+                content.innerHTML = interpolateVariables(content.originalHTML);
+                content.innerHTML = evaluateDataChecks(content.innerHTML);
+                game.initializeGameNavListeners();
+            }
+            let control_types = ["input", "textarea", "select"];
+            for (const control_type of control_types) {
+                let control_fields = content.querySelectorAll(control_type);
+                for (const control_field of control_fields) {
+                    let target_object = null;
+                    let target_name = control_field.getAttribute("data-for");
+                    if (typeof target_name != "undefined") {
+                        if (game.objects[target_name]) {
+                            target_object = game.objects[target_name];
+                        } else if (game.locations[target_name]) {
+                            target_object = game.locations[target_name];
+                        }
+                        if (target_object) {
+                            let target_field = control_field.getAttribute(
+                                "name");
+                            let control_subtype = '';
+                            if (target_field) {
+                                if (control_type == "input") {
+                                    control_subtype =
+                                        control_field.getAttribute("type");
+                                    if (control_subtype == 'radio') {
+                                        if (control_field.value ==
+                                            target_object[target_field]) {
+                                            control_field.checked =
+                                                target_object[target_field];
+                                        }
+                                    } else {
+                                        control_field.value =
+                                            target_object[target_field];
+                                    }
+                                } else if (target_object[target_field]) {
+                                    /* if don't check
+                                       target_object[target_field] not empty
+                                       then select tags get extra blank option
+                                     */
+                                    control_field.value =
+                                        target_object[target_field];
+                                }
+                                if(!control_field.disabled) {
+                                    if (control_type == "select") {
+                                        control_field.addEventListener("change",
+                                        (evt) => {
+                                            target_object[target_field] =
+                                                control_field.value;
+                                        });
+                                    } else if (control_subtype == "radio") {
+                                        control_field.addEventListener("click",
+                                        (evt) => {
+                                            if (control_field.checked) {
+                                                target_object[target_field] =
+                                                    control_field.value;
+                                            }
+                                        });
+                                    } else {
+                                        control_field.addEventListener("input",
+                                        (evt) => {
+                                            target_object[target_field] =
+                                                control_field.value;
+                                        });
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    /**
+     * Evaluates the condition in a ck or else-ck attribute of an x-present tag.
+     *
+     * @param {string} condition contents from a ck, check, else-ck,
+     *      or else-check attribute.  Conditions can be boolean conditions
+     *      on game variables. If an x-present tag did not have a ck attribute,
+     *      condition is null.
+     * @param {string} staging contents from a stage attribute.
+     *      If no such attribute, this will be an empty string.
+     *      Such an attribute could have a sequence of
+     *      pause(some_millisecond); and clickProceed(some_string) commands
+     * @return {Array} [check_result, proceed, pause, typing] if the condition
+     *  involved   a boolean expression, then check_result will hold the result
+     *  of   the expression (so the caller then could prevent the the display of
+     *  an x-present tag if false), proceed is the link text (if any) for a
+     *  link for the first clickProceed (which is supposed to delay the
+     *  presentation of the x-present tag until after the user clicks the
+     *  link) is found (else ""), pause (if non zero) is the number of
+     *  milliseconds to sleep before presenting the x-present tag according to
+     *  the condition,
+     */
+    evaluateCheckConditionStaging(condition, staging)
+    {
+        let proceed = "";
+        let pause = 0;
+        condition = (typeof condition == "string") ? condition : "";
+        let check_result = (condition.replace(/\s+/, "") != "") ?
+            eval(condition) : true;
+        if (typeof check_result != "boolean") {
+            check_result = false;
+            console.log(condition + " didn't evaluate to a boolean");
+        }
+        let staging_remainder = staging;
+        let old_staging = "";
+        while (check_result && old_staging != staging_remainder) {
+            old_staging = staging_remainder;
+            let click_pattern = /clickProceed\([\'\"]([^)]+)[\'\"]\);?/;
+            let click_match = staging_remainder.match(click_pattern);
+            if (click_match) {
+                proceed = click_match[1];
+                break;
+            }
+            let pause_pattern = /pause\(([^)]+)\);?/;
+            let pause_match = staging_remainder.match(pause_pattern);
+            if (pause_match) {
+                pause += parseInt(pause_match[1]);
+            }
+        }
+        return [check_result, proceed, pause];
+    }
+    /**
+     * A given Location contains one or more x-present tags which are
+     * used when rendering that location to the game-content area. This
+     * method takes the text from one such tag, interpolates any game
+     * variables into it, and adds to any x-speaker tags in it the HTML
+     * code to render that speaker bubble (adds img tag for speaker icon, etc).
+     *
+     * @param {string} section text to process before putting into the
+     *  game content area.
+     * @return {string} resulting HTML text after interpolation and processing
+     */
+    prepareSection(section)
+    {
+        let old_section = "";
+        let quote = `(?:(?:'([^']*)')|(?:"([^"]*)"))`;
+        section = evaluateDataChecks(section);
+        section = interpolateVariables(section);
+        while (section != old_section) {
+            old_section = section;
+            let speaker_pattern = new RegExp(
+                "\<x-speaker([^\>]*)name\s*\=\s*(" + quote + ")([^\>]*)>",
+                'i');
+            let expression_pattern = new RegExp(
+                "expression\s*\=\s*("+quote+")", 'i');
+            let speaker_match = section.match(speaker_pattern);
+            if (speaker_match) {
+                let speaker_id = speaker_match[4];
+                let pre_name = (speaker_match[3]) ? speaker_match[3] : "";
+                let post_name = (speaker_match[5]) ? speaker_match[5] : "";
+                let rest_of_tag = pre_name + " " + post_name;
+                let expression_match = rest_of_tag.match(expression_pattern);
+                if (speaker_id && game.objects[speaker_id]) {
+                    let speaker = game.objects[speaker_id];
+                    let name = speaker.name;
+                    if (expression_match && expression_match[3]) {
+                        name += " <b>(" + expression_match[3] + ")</b>";
+                    }
+                    let icon = speaker.icon;
+                    let html_fragment = speaker_match[0];
+                    let html_fragment_mod = html_fragment.replace(/name\s*\=/,
+                        "named=");
+                    if (icon) {
+                        section =
+                            section.replace(html_fragment, html_fragment_mod +
+                            "<figure><img src='" + speaker.icon + "' " +
+                            "loading='lazy' ></figure><div>" + name +
+                            "</div><hr>");
+                    }
+                }
+            }
+        }
+        return section;
+    }
+}
+/**
+ * Encapsulates one paperdoll that might be drawn or animated to
+ * a canvas in the Game. A Doll has a width and height and either
+ * an image or background color. It also has a number of DollSlot
+ * subrectangles, on which other images or solid colors may be drawn.
+ * DollSlots can be animated if they responds to update events, can collide
+ * if respond to collision events, or can be clicked on if they respond to
+ * click events. A Doll can be navigable if it has a player dollslot
+ * and scrollable if it is vaigable and its dimension are large than its
+ * viewable area.
+ */
+class Doll
+{
+    /**
+     * Whether the current Doll or any of its DollSlot objects respond to
+     * click events
+     * @type {boolean}
+     */
+    has_click_listeners = false;
+    /**
+     * Whether the current Doll or any of its DollSlot objects have an update
+     * listener specified to be called according to the update time function
+     * @type {boolean}
+     */
+    has_update_listeners = false;
+    /**
+     * Whether the current Doll or any of its DollSlot objects respond to
+     * collision between DollSlot events
+     * @type {boolean}
+     */
+    has_collision_listeners = false;
+    /**
+     * Number of images in DollSlot object on this Doll that
+     * have yet to load
+     * @type {int}
+     */
+    num_loading;
+    /**
+     * If the Doll is navigable, then this holds the index of the PlayerSlot
+     * in the array this.slots.items of DollSlot objects
+     * @type {int|boolean}
+     */
+    player_index = false;
+    /**
+     * An array of canvas objects on which this Doll has been requested to
+     * render, but can't yet (say because relies on images which are still
+     * loading)
+     * @type {Array<Canvas>}
+     */
+    render_requests;
+    /**
+     * Is true if the Doll is currently being drawn in some other thread
+     * @type {boolean}
+     */
+    rendering;
+    /**
+     * Checks for any collisions between DollSlot objects on the current Doll
+     * that have collision listeners specified. If there are then the
+     * listeners are called with an event containing info about the two
+     * DollSlots involved and the Dollhouse on which the Doll will be rendered.
+     *
+     * @param {Dollhouse} dollhouse on which the current Doll is to be rendered
+     */
+    checkCollisions(dollhouse)
+    {
+        if (this['collision-listener']) {
+            eval(this['collision-listener']);
+        }
+        let items = this.slots.items;
+        let len = items.length;
+        if (len == 1) {
+            return;
+        }
+        for (let i = 0; i < len; i++) {
+            for (let j = i + 1; j < len; j++) {
+                let s1 = items[i];
+                let s2 = items[j];
+                if (typeof s1 == 'undefined' ||
+                    typeof s2 == 'undefined' ||
+                    (!s1['collision-listener'] && !s2['collision-listener'])) {
+                    continue;
+                }
+                let rect = intersectRectangles(
+                    [s1.x, s1.y, s1.width, s1.height],
+                    [s2.x, s2.y, s2.width, s2.height]);
+                if (rect) {
+                    var event = {
+                        'a' : s1,
+                        'b' : s2,
+                        'dollhouse' : dollhouse
+                    };
+                    if (s1['collision-listener']) {
+                        eval(s1['collision-listener']);
+                    }
+                    if (s2['collision-listener']) {
+                        eval(s2['collision-listener']);
+                    }
+                }
+            }
+        }
+    }
+    /**
+     * Returns the DollSlot used to manage the player's coordinates and
+     * icon image within the current Doll (provided the Doll is navigable).
+     * If the player DollSlot doesn't exist an attempt is made to create it.
+     * If a Dollhouse is provided this is used in the attempt to create the
+     * player slot.
+     *
+     * @param {DollHouse?} dollhouse that may be used to help create the player
+     *  DollSlot
+     * @return {DollSlot} the player DollSlot
+     */
+    getPlayerSlot(dollhouse = null)
+    {
+        let player_slot;
+        let original_dh = dollhouse;
+        dollhouse ??= {'player-width': 50, 'player-height': 50,
+            'player-x': -1, 'player-x' : -1};
+        if (this.player_index === false) {
+            player_slot = new DollSlot();
+            player_slot.type = 'DollSlot';
+            this.setPlayerSlot(player_slot);
+        }
+        player_slot = this.slots.items[this.player_index];
+        if (dollhouse) {
+            parseObjectsInts([[dollhouse, ['player-width', 'player-height',
+                'player-x', 'player-x']]]);
+        }
+        player_slot.width ??= dollhouse['player-width'];
+        player_slot.height ??= dollhouse['player-height'];
+        player_slot.x ??= (dollhouse['player-x'] >= 0) ? dollhouse['player-x'] -
+            Math.floor(player_slot.width/2) :
+            Math.floor((this.width - player_slot.width)/2);
+        player_slot.y ??= (dollhouse['player-y']  >= 0) ?
+            dollhouse['player-y'] - Math.floor(player_slot.height/2) :
+            Math.floor((this.height - player_slot.height)/2);
+        if (typeof mc().icon !== 'undefined') {
+            player_slot.icon = mc().icon;
+        } else {
+            player_slot.color = (dollhouse['player-color']) ?
+                dollhouse['player-color'] : 'blue';
+        }
+        if (original_dh) {
+            player_slot.init(this);
+        }
+        this.setPlayerSlot(player_slot);
+        return player_slot;
+    }
+    /**
+     * Used to set up the Doll. This involves loading all images related to
+     * doll, and initializing the has_click_listeners, has_collision_listeners,
+     * has_update_listeners booleant
+     */
+    init()
+    {
+        this.num_loading = 0;
+        this.render_requests = [];
+        this.rendering = false;
+        if (this.icon) {
+            this.image = this.loadImage(this.icon);
+        }
+        this.slots.init(this);
+        let listener_has_map = {
+            'click-listener' : 'has_click_listeners',
+            'collision-listener' : 'has_collision_listeners',
+            'update-listener' : 'has_update_listeners'
+        };
+        for (const listener in listener_has_map) {
+            let has_listener = listener_has_map[listener];
+            if (this[listener] || this.slots[has_listener]) {
+                this[has_listener] =  true;
+            }
+        }
+    }
+    /**
+     * Initiates loading of the image at URL into an Image object and returns
+     * this object. Adds one to num_loading property of this Doll,
+     * so know Doll is tracking one more image to be loaded.
+     * Sets up a callback such that if all the images that have been requested
+     * for this Doll have loaded, and there have been requests to render
+     * this doll, then the Doll is rendered to each canvas requested.
+     *
+     * @param {string} URL of image to load
+     * @return {HTMLImageElement} image to be loaded
+     */
+    loadImage(url)
+    {
+        let image = new Image();
+        image.src = url;
+        this.num_loading++;
+        image.onload = () => {
+            this.num_loading--;
+            if (this.num_loading <= 0) {
+                this.num_loading = 0;
+            } else {
+                return;
+            }
+            if (this.render_requests.length > 0 && !this.rendering) {
+                this.rendering = true;
+                for (const canvas of this.render_requests) {
+                    this.render(canvas);
+                }
+                this.render_requests = [];
+                this.rendering = false;
+            }
+        }
+        return image;
+    }
+    /**
+     * If the passed slot is a DollSlot adds it to slots and returns true;
+     * otherwise, does nothing and returns false
+     * @param {DollSlot} slot to insert
+     * @return {boolean} whether the item was added or not
+     */
+    push(slot)
+    {
+        return this.slots.push(slot);
+    }
+    /**
+     * Draws the current Doll to the provided canvas view. Note given the
+     * Doll's coordinate system only some portion of the Doll may be
+     * visible. If not all of the images for this Doll have been downloaded
+     * a render request is made instead.
+     *
+     * @param {HTMLCanvasElement} canvas to draw Doll onto
+     */
+    render(canvas)
+    {
+        if (!canvas) {
+            return;
+        }
+        if (this.num_loading > 0) {
+            this.render_requests.push(canvas);
+            return;
+        }
+        const context = canvas.getContext("2d");
+        let source_x = parseInt(canvas.getAttribute('data-source-x'));
+        let source_y = parseInt(canvas.getAttribute('data-source-y'));
+        let source_width = parseInt(canvas.getAttribute('data-source-width'));
+        let source_height = parseInt(canvas.getAttribute('data-source-height'));
+        let view_width = parseInt(canvas.getAttribute('width'));
+        let view_height = parseInt(canvas.getAttribute('height'));
+        if (is_mobile) {
+            let max_mobile_width = window.screen.width - 40;
+            let max_mobile_height = view_height * max_mobile_width / view_width;
+            if (view_width > max_mobile_width) {
+                view_width = max_mobile_width;
+                view_height = max_mobile_height;
+                canvas.width = view_width;
+                canvas.height = view_height;
+            }
+        }
+        let color = this.color ?? 'white';
+        context.clearRect(0, 0, view_width, view_height);
+        let scale_x = view_width/source_width;
+        let scale_y = view_height/source_height;
+        if (this.image) {
+            let rect = intersectRectangles(
+                [source_x, source_y, source_width, source_height],
+                [0, 0, source_width, source_height]);
+            let coords = imageCoordinates(rect[0], rect[1],
+                rect[2], rect[3], this);
+            let dest_rect = [Math.floor(scale_x * (rect[0] - source_x)),
+                Math.floor(scale_y * (rect[1] - source_y)),
+                Math.floor(scale_x * rect[2]),
+                Math.floor(scale_y * rect[3])
+            ];
+            context.drawImage(this.image, coords[0], coords[1], coords[2],
+                coords[3], dest_rect[0], dest_rect[1], dest_rect[2],
+                dest_rect[3]);
+        } else if (this.color) {
+            context.fillStyle = this.color;
+            context.fillRect(0, 0, view_width, view_height);
+        }
+        let doll_slots = this.slots.items;
+        if (!doll_slots) {
+            return;
+        }
+        for (const doll_slot of doll_slots) {
+            if (typeof doll_slot == 'undefined') {
+                continue;
+            }
+            let rect = intersectRectangles(
+                [source_x, source_y, source_width, source_height],
+                [doll_slot.x, doll_slot.y, doll_slot.width, doll_slot.height]);
+            if (rect) {
+                let dest_rect = [Math.floor(scale_x * (rect[0] - source_x)),
+                    Math.floor(scale_y * (rect[1] - source_y)),
+                    Math.floor(scale_x * rect[2]),
+                    Math.floor(scale_y * rect[3])
+                ];
+                if (doll_slot.image) {
+                    let coords = imageCoordinates(rect[0], rect[1],
+                        rect[2], rect[3], doll_slot, true);
+                    context.drawImage(doll_slot.image,
+                        coords[0], coords[1],
+                        coords[2], coords[3], dest_rect[0], dest_rect[1],
+                        dest_rect[2], dest_rect[3]);
+                } else {
+                    context.fillStyle = doll_slot.color;
+                    context.fillRect(dest_rect[0], dest_rect[1],
+                        dest_rect[2], dest_rect[3]);
+                }
+            }
+        }
+    }
+    /**
+     * Sets the DollSlot object that corresponds to the person playing the
+     * game to player_slot. This object holds player info when a Doll is
+     * navigable
+     *
+     * @param {DollSlot} player_slot a doll slot with player's rectangle info
+     *  on the doll, and icon or color to draw for player
+     */
+    setPlayerSlot(player_slot)
+    {
+        if (this.player_index === false) {
+            this.player_index = this.push(player_slot);
+        } else {
+            this.slots.items[this.player_index] = player_slot;
+        }
+    }
+}
+
+/**
+ * Encapsulates the image slots a Doll might have
+ */
+class DollSlots
+{
+    /**
+     * Whether any of the DollSlot object managed by this DollSlots has
+     * a click listener. Such a click listener is called if a the
+     * canvas used to draw a Doll is clicked and that click is within the
+     * rectangle of the listening DollSlot.
+     * @type {boolean}
+     */
+    has_click_listeners = false;
+    /**
+     * Whether any of the DollSlot object managed by this DollSlots has
+     * a collision listener. Such a listener is called if the rectangle
+     * of a listening DollSlot intersects with another DollSlot in this
+     * DollSlots.
+     * @type {boolean}
+     */
+    has_collision_listeners = false;
+    /**
+     * Whether any of the DollSlot object managed by this DollSlots has
+     * an update listener a timer is used to call such listeners periodically
+     * @type {boolean}
+     */
+    has_update_listeners = false;
+    /**
+     * Array of DollSlot objects managed by this DollSlots.
+     * @type {Array}
+     */
+    items = [];
+    /**
+     * If not null, Doll on which this DollSlot lives.
+     * @type {Doll?}
+     */
+    parent;
+    /**
+     * Returns the DollSlot at index i if it exists else return null
+     * @param {int} DollSlot index to get
+     * @return {DollSlot?} the desired DollSlot if present
+     */
+    get(i)
+    {
+        if (typeof this.items[i] != 'undefined') {
+            return this.items[i];
+        }
+        return null;
+    }
+    /**
+     * Calls init on each of the DollSlots in items. Initializes the
+     * has_click_listeners, has_collision_listeners, and has_update_listeners
+     * property based on checking for the corresponding listeners in
+     * each DollSlot iterated over.
+     *
+     * @param {Doll?} parent Doll used to help initialize each Doll (used
+     *   for Image loading (Doll needs to keep track of how many of its
+     *   DollSlots still need to be loaded))
+     */
+    init(parent)
+    {
+        let listener_has_map = {
+            'click-listener' : 'has_click_listeners',
+            'collision-listener' : 'has_collision_listeners',
+            'update-listener' : 'has_update_listeners'
+        };
+        for (const doll_slot of this.items) {
+            if (doll_slot.type == 'DollSlot') {
+                doll_slot.init(parent);
+                for (const listener in listener_has_map) {
+                    let has_listener = listener_has_map[listener];
+                    if (doll_slot[listener]) {
+                        this[has_listener] = true;
+                    }
+                }
+            }
+        }
+    }
+    /**
+     * If the passed slot is a DollSlot adds it to items Array at index i
+     * and return true; otherwise, does nothing and returns false
+     * @param {int} i index in items array to insert DollSlot
+     * @param {DollSlot} slot to insert
+     * @return {boolean} whether the item was added or not
+     */
+    set(i, slot)
+    {
+        if (typeof slot['type'] != 'undefined' && slot['type'] == 'DollSlot') {
+            this.items[i] = slot;
+            return i;
+        } else {
+            return false;
+        }
+    }
+    /**
+     * If the passed slot is a DollSlot adds it to items Array and returns true;
+     * otherwise, does nothing and returns false
+     * @param {DollSlot} slot to insert
+     * @return {boolean} whether the item was added or not
+     */
+    push(slot)
+    {
+        return this.set(this.items.length, slot);
+    }
+    /**
+     * Removes and delete the DollSlot at location index from this DollSlots
+     * items array of DollSlot objects
+     * @param {int} index of DollSlot to remove
+     */
+    remove(index)
+    {
+        if (typeof this.items[i] != 'undefined') {
+            delete this.items[index];
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Removes the DollSlot of the given slot_id name from this DollSlots object
+     *
+     * @param {string} slot_id id property of the DollSlot to remove
+     * @return {boolean} true if a DollSlot with the given id is found and
+     *  removed, false otherwise
+     */
+    removeById(slot_id)
+    {
+        for (const i = 0; i < this.items.length; i++) {
+            if (this.items[i] && this.items[i].id == slot_id) {
+                delete this.items[i];
+                return true;
+            }
+        }
+        return false;
+    }
+}
+
+/**
+ * Encapsulates a single image slot for use in a Doll
+ */
+class DollSlot
+{
+    /**
+     * The filename or object to be used by this slot in a paper Doll
+     * @type {}
+     */
+    icon = "";
+    /**
+     * The image to be used by this slot in a paper Doll
+     * @type {Image}
+     */
+    image = null;
+    /**
+     * The height of the slot in a paper Doll
+     * @type {int}
+     */
+    height;
+    /**
+     * The width of the slot in a paper Doll
+     * @type {int}
+     */
+    width;
+    /**
+     * The x coordinate within the overall Doll for this DollSlot
+     * @type {int}
+     */
+    x;
+    /**
+     * The y coordinate within the overall Doll for this DollSlot
+     * @type {int}
+     */
+    y;
+    /**
+     * Initializes this DollSlot within its parent Doll, parent_doll.
+     * This involves loading any Image associated with this DollSlot
+     * or discarding any image, if the DollSlot is going to be presented
+     * as a solid color.
+     */
+    init(parent_doll)
+    {
+        if (this.icon) {
+            this.image = parent_doll.loadImage(this.icon);
+        } else if (this.image) {
+            delete this.image;
+        }
+    }
+}
+/**
+ * Class used to encapsulate an interactive story game. It has fields
+ * to track the locations and objects in the game, the history of moves of
+ * the game, and how many moves have been made. It has methods to
+ * take a turn in such a game, to save state, load state,
+ * restore prev/next state from history, render the state of such
+ * a game.
+ */
+class Game
+{
+    /**
+     * If there are any dollhouses currently being drawn which are updateable
+     * this gives the default refresh rate in milliseconds
+     * @type {int}
+     */
+    dollhouse_refresh_rate = 50;
+    /**
+     * ids of keyboard and setTimeouts that are currently updating
+     * dollhouses (if any) in the the location being presented
+     * @type {Array<int>}
+     */
+    dollhouse_update_ids = [];
+    /**
+     * A semi-unique identifier for this particular game to try to ensure
+     * two different games hosted in the same folder don't collide in
+     * sessionStorage.
+     * @type {string}
+     */
+    id;
+    /**
+     * Whether game page was just reloaded
+     * @type {boolean}
+     */
+    reload;
+    /**
+     * Current date followed by a space followed by the current time of
+     * the most recent game capture. Used in providing a description of
+     * game saves.
+     * @type {number}
+     */
+    timestamp;
+    /**
+     * A counter that is incremented each time Javascript draws a new
+     * clickProceed a tag. Each such tag is given an id, tick is used to ensure
+     * these id's are unique.
+     * @type {number}
+     */
+    tick = 0;
+    /**
+     * Whether this particular game has a nav bar or not
+     * @type {boolean}
+     */
+    has_nav_bar;
+    /**
+     * List of all Game Object's managed by the FRISE script. An object
+     * can be used to represent a thing such as a person, tool, piece of
+     * clothing, letter, etc. In an HTML document, a game object is defined
+     * using  an x-object tag.
+     * @type {Array<Object>}
+     */
+    objects;
+    /**
+     * List of all game Location's managed by the FRISE script. A Location
+     * can be used to represent a place the main character can go. This
+     * could be standard locations in the game, as well as Locations
+     * like a Save page, Inventory page, Status page, etc.
+     * In an HTML document a game Location is defined using an x-location tag.
+     * @type {Array<Location>}
+     */
+    locations;
+    /**
+     *
+     *
+     * @type {Array<Doll>}
+     */
+    dolls;
+    /**
+     * Used to maintain a stack (using Array push/pop) of Game State Objects
+     * based on the turns the user has taken (the top of the stack corresponds
+     * to the previous turn). A Game State Object is a serialized string:
+     *  {
+     *    timestamp: capture_time,
+     *    objects: array_of_game_objects_at_capture_time,
+     *    locations: array_of_game_locations_at_capture_time,
+     *  }
+     * @type {Array}
+     */
+    history;
+    /**
+     * Used to maintain a stack (using Array push/pop) of Game State Objects
+     * based on the the number previous turn clicks the user has done.
+     * I.e., when a user clicks previous turn, the current state is pushed onto
+     * this array so that if the user then clicks next turn the current
+     * state can be restored.
+     * A Game State Object is a serialized string:
+     *  {
+     *    timestamp: capture_time,
+     *    objects: array_of_game_objects_at_capture_time,
+     *    locations: array_of_game_locations_at_capture_time,
+     *  }
+     * @type {Array}
+     */
+    future_history;
+    /**
+     * Id of first room main-character is in;
+     * @type {String}
+     */
+    base_location;
+    /**
+     * Is set to true just before a default action for a location the
+     * main character is at is executed; otherwise, false
+     * @type {Boolean}
+     */
+    is_here;
+    /**
+     * List of id's of buttons and links to disable during the staging
+     * phases of rendering a presentation or when viewing those locations.
+     * @type {Array}
+     */
+    volatile_links = ['saves', 'inventory'];
+    /**
+     * Sets up a game object with empty history, an initialized main navigation,
+     * and with objects and locations parsed out of the current HTML file
+     */
+    constructor()
+    {
+        let title_elt = tag('title')[0];
+        if (!title_elt) {
+            title_elt = tag('x-game')[0];
+        }
+        this.reload = false; //current
+        let doc_length = 0;
+        let middle_five = "";
+        if (title_elt) {
+            doc_length = title_elt.innerHTML.length;
+            if (doc_length > 8) {
+                let half_length = Math.floor(doc_length/2);
+                middle_five = title_elt.innerHTML.slice(
+                    half_length, half_length + 5);
+            }
+        }
+        // a semi-unique code for this particular game
+        this.id = encodeURI(middle_five + doc_length);
+        this.initializeMainNavGameContentArea();
+        this.initializeObjectsLocationsDolls();
+        this.clearHistory();
+    }
+    /**
+     * Writes to console information about which objects and locations
+     * might not be properly defined.
+     */
+    debug()
+    {
+        let none = "none";
+        console.log("Game objects without position:");
+        for (let obj of Object.values(this.objects)) {
+            if (!obj.has_position) {
+                console.log("  " + obj.id);
+                none = "";
+            }
+        }
+        if (none) {
+            console.log("  " + none);
+        }
+        none = "none";
+        console.log("Game locations without x-present:");
+        for (loc of  Object.values(this.locations)) {
+            if (!loc.has_present) {
+                console.log("  " +loc.id);
+                none = "";
+            }
+        }
+        none = "none";
+        console.log("Game dolls missing required attributes:");
+        for (const doll_obj of  Object.values(this.dolls)) {
+            if (!doll_obj.has_icon && !doll_obj.has_color) {
+                console.log("  " + doll_obj.icon + " has no icon or color");
+                none = "";
+            }
+            if (!doll.has_height) {
+                console.log("  " + doll_obj.height + " has no height");
+                none = "";
+            }
+            if (!doll.has_width) {
+                console.log("  " + doll_obj.width + " has no width");
+                none = "";
+            }
+        }
+        if (none) {
+            console.log("  " + none);
+        }
+        return true;
+    }
+    /**
+     * Used to reset the game to the condition at the start of a game
+     */
+    reset()
+    {
+        sessionStorage.removeItem("current" + this.id);
+        this.initializeMainNavGameContentArea();
+        this.initializeObjectsLocationsDolls();
+        this.clearHistory();
+    }
+    /**
+     * Sets up the main navigation bar and menu on the side of the screen
+     * determined  by the is_right_to_left variable. Sets up an initially empty
+     * game content area which can be written to by calling a Location
+     * object's renderPresentation. The main navigation consists of a hamburger
+     * menu toggle button for the navigation as well as previous
+     * and next history arrows at the top of screen. The rest of the main
+     * navigation content is determined by the contents of the x-main-nav
+     * tag in the HTML file for the game. If this tag is not present, the
+     * game will not have a main navigation bar and menu.
+     */
+    initializeMainNavGameContentArea()
+    {
+        let body_objs = tag("body");
+        if (body_objs[0] === undefined) {
+            return;
+        }
+        let body_obj = body_objs[0];
+        let game_screen = elt('game-screen');
+        if (!game_screen) {
+            body_obj.innerHTML = '<div id="game-screen"></div>' +
+                body_obj.innerHTML;
+            game_screen = elt('game-screen');
+        }
+        let main_nav_objs = tag("x-main-nav");
+        if (typeof main_nav_objs[0] === "undefined") {
+            game_screen.innerHTML = `<div id="game-content" tabindex="0">
+                </div>`;
+            this.has_nav_bar = false;
+            return;
+        }
+        this.has_nav_bar = true;
+        let main_nav_obj = main_nav_objs[0];
+        let history_buttons;
+        if (is_right_to_left) {
+            history_buttons =
+                `<button id="previous-history">→</button>
+                 <button id="next-history">←</button>`;
+        } else {
+            history_buttons =
+                `<button id="previous-history">←</button>
+                 <button id="next-history">→</button>`;
+        }
+        game_screen.innerHTML = `
+            <div id="main-bar">
+            <button id="main-toggle"
+            class="float-left"><span class="main-close">≡</button>
+            </div>
+            <div id="main-nav">
+            ${history_buttons}
+            <div id="game-nav">
+            ${main_nav_obj.innerHTML}
+            </div>
+            </div>
+            <div id="game-content"></div>`;
+        this.initializeGameNavListeners();
+    }
+    /**
+     * Used to initialize the event listeners for the next/previous history
+     * buttons. It also adds listeners to all the a tag and x-button tags
+     * to process their href attributes before following any link is followed
+     * to its target.
+     * @see addListenersAnchors
+     */
+    initializeGameNavListeners()
+    {
+        elt('main-toggle').onclick = (evt) => {
+            toggleMainNav('main-nav', 0);
+        };
+        elt('next-history').onclick = (evt) => {
+            this.nextHistory();
+        }
+        elt('previous-history').onclick = (evt) => {
+            this.previousHistory();
+        }
+        let anchors = sel('#game-nav a, #game-nav x-button');
+        addListenersAnchors(anchors, is_mobile);
+    }
+    /**
+     * Checks if the game is being played on a mobile device. If not, this
+     * method does nothing, If it is being played on a mobile device,
+     * then this method sets up the viewport so that the HTML will
+     * display properly. Also, in the case where the game is being played on a
+     * mobile device, this method also sets it so the main nav bar on the side
+     * of the screen is closed.
+     */
+    initializeScreen()
+    {
+        let html = tag("html")[0];
+        if (is_right_to_left) {
+            html.classList.add("rtl");
+        }
+        if (!this.has_nav_bar) {
+            html.classList.add("no-nav");
+        }
+        let is_slides = sel("x-slides")[0] ? true : false;
+        if (is_slides) {
+            html.classList.add("slides");
+        }
+        if(!is_mobile) {
+            return;
+        }
+        html.classList.add("mobile");
+        let head = tag("head")[0];
+        head.innerHTML += `<meta name="viewport" `+
+            `content="width=device-width, initial-scale=1.0" >`;
+        if (this.has_nav_bar) {
+            toggleMainNav();
+        }
+    }
+    /**
+     * For each object, if object.position is defined, then adds the object
+     * to the location.item array of the Location whose id is
+     * given by object.position. Sets up the dolls Array of paperdolls
+     * used in the game (so their images start loading).
+     */
+    initializeObjectsLocationsDolls()
+    {
+        this.objects = xtag("x-object");
+        this.locations = xtag("x-location");
+        this.dolls = xtag('x-doll');
+        for (const oid in this.objects) {
+            let object = this.objects[oid];
+            if (object.hasOwnProperty("position")) {
+                let location_name = object.position;
+                if (this.locations.hasOwnProperty(location_name)) {
+                    let location = this.locations[location_name]
+                    if (!location.hasOwnProperty("items")) {
+                        location.items = [];
+                    }
+                    location.items.push(object.id);
+                }
+                if (typeof object.original_position == 'undefined') {
+                    object.original_position = object.position;
+                }
+            }
+        }
+        for (const lid in this.locations) {
+            let location = this.locations[lid];
+            if (!location.hasOwnProperty("items")) {
+                location.items = [];
+            }
+            if (typeof location.original_items == 'undefined') {
+                location.original_items = location.items;
+            }
+        }
+    }
+    /**
+     * Creates a JSON encoded string representing the current state of
+     * the game (all of the object and location states and where the main
+     * character is).
+     *
+     * @return {string} JSON encoded current state of game.
+     */
+    captureState()
+    {
+        let now = new Date();
+        let date = now.getFullYear() + '-' + (now.getMonth() + 1) +
+            '-' + now.getDate();
+        let time = now.getHours() + ":" + now.getMinutes() + ":"
+                + now.getSeconds();
+        game.timestamp = date + " " + time;
+        return JSON.stringify({
+            timestamp: game.timestamp,
+            base_location: game.base_location,
+            objects: this.objects,
+            locations: this.locations
+        });
+    }
+    /**
+     * Sets the current state of the game (current settings for all objects,
+     * locations, and main character position), based on the state given in
+     * a JSON encode string representing a game state.
+     *
+     * @param {string} gave_save a JSON encoded state of the a FRISE game.
+     */
+    restoreState(game_save)
+    {
+        let game_state = JSON.parse(game_save);
+        if (!game_state || !game_state.timestamp ||
+            !game_state.objects || !game_state.locations) {
+            alert(tl[locale]['restore_state_invalid_game']);
+            return false;
+        }
+        this.timestamp = game_state.timestamp;
+        this.base_location = game_state.base_location;
+        /*
+          during development, changing an object or location's text might
+          not be viewable on a reload unless we copy some fields of the
+          reparsed html file into a save game object.
+         */
+        let old_objects = this.objects;
+        this.objects = game_state.objects;
+        for (const field in old_objects) {
+            if (!this.objects.hasOwnProperty(field)) {
+                /* we assume our game never deletes objects or locations, so
+                   if we find an object in old_objects (presumably it's coming
+                   from a more recently parsed HTML file) that was not
+                   in the saved state, we copy it over.
+                 */
+                this.objects[field] = old_objects[field];
+            } else {
+                if (old_objects.hasOwnProperty('action')) {
+                    this.objects['action'] = old_objects['action'];
+                } else if (this.objects.hasOwnProperty('action')) {
+                    delete this.objects['action'];
+                }
+            }
+        }
+        let old_locations = this.locations;
+        let locations = game_state.locations;
+        let location;
+        this.locations = {};
+        for (const location_name in old_locations) {
+            if (!locations.hasOwnProperty(location_name)) {
+                location = old_locations[location_name];
+            } else {
+                let location_object = locations[location_name];
+                location = new Location();
+                for (const field in location_object) {
+                    location[field] = location_object[field];
+                    if (field == 'present' || field == 'action' ||
+                        field == 'default-action') {
+                        if (!old_locations[
+                            location_name].hasOwnProperty(field)) {
+                            delete location[field];
+                        } else {
+                            location[field] =
+                                old_locations[location_name][field];
+                        }
+                    }
+                }
+            }
+            this.locations[location_name] = location;
+        }
+        return true;
+    }
+    /**
+     * Deletes the game state capture history for the game. After this
+     * calling this method, the game's next and previous arrow buttons
+     * won't do anything until new turns have occurred.
+     */
+    clearHistory()
+    {
+        this.history = [];
+        this.future_history = [];
+        let next_history_elt = elt('next-history');
+        if (next_history_elt) {
+            next_history_elt.disabled = true;
+            elt('previous-history').disabled = true;
+        }
+    }
+    /**
+     * Called when the left arrow button on the main nav page is
+     * clicked to go back one turn in the game history. Pushes the current
+     * game state to the future_history game state array, then pops the most
+     * recent game state from the history game state array and sets it as
+     * the current state.
+     */
+    previousHistory()
+    {
+        if (this.history.length == 0) {
+            return;
+        }
+        let current_state = this.captureState();
+        this.future_history.push(current_state);
+        let previous_game_state = this.history.pop();
+        this.restoreState(previous_game_state);
+        sessionStorage["current" + this.id] = previous_game_state;
+        this.describeMainCharacterLocation();
+        if (this.history.length == 0) {
+            elt('previous-history').disabled = true;
+        } else {
+            elt('previous-history').disabled = false;
+        }
+        elt('next-history').disabled = false;
+    }
+    /**
+     * Called when the right arrow button on the main nav page is
+     * clicked to go forward one turn in the game history (assuming the user had
+     * clicked previous at least once). Pushes the current game state
+     * to the history game state array, then pops the game state from the
+     * future_history game state array and sets it as the current state.
+     */
+    nextHistory()
+    {
+        if (this.future_history.length == 0) {
+            return;
+        }
+        let current_state = this.captureState();
+        this.history.push(current_state);
+        let next_game_state = this.future_history.pop();
+        this.restoreState(next_game_state);
+        sessionStorage["current" + this.id] = next_game_state;
+        this.describeMainCharacterLocation();
+        if (this.future_history.length == 0) {
+            elt('next-history').disabled = true;
+        } else {
+            elt('next-history').disabled = false;
+        }
+        elt('previous-history').disabled = false;
+    }
+    /**
+     * Initializes the save slots for the saves location page of a game.
+     * This involves looking at session storage and determining which slots
+     * have games already saved to them, and for those slots, determining also
+     * what time the game was saved.
+     */
+    initSlotStates()
+    {
+        let saves_location = game.locations['saves'];
+        for (const field in saves_location) {
+            let slot_matches = field.match(/^slot(\d+)/);
+            if (slot_matches && slot_matches[1]) {
+                let slot_number = parseInt(slot_matches[1]);
+                let game_save = localStorage.getItem("slot" + game.id
+                    + slot_number);
+                if (game_save) {
+                    let game_state = JSON.parse(game_save);
+                    saves_location["slot" + slot_number] =
+                        tl[locale]['init_slot_states_load'];
+                    saves_location["delete" + slot_number] = "";
+                    saves_location["filled" + slot_number] = 'filled';
+                    saves_location["filename" + slot_number] =
+                        game_state.timestamp;
+                } else {
+                    saves_location["slot" + slot_number] =
+                        tl[locale]['init_slot_states_save'];
+                    saves_location["filled" + slot_number] = 'not-filled';
+                    saves_location["delete" + slot_number] = "disabled";
+                    saves_location["filename" + slot_number] = '...';
+                }
+            }
+        }
+    }
+    /**
+     * Saves the current game state to a localStorage save slot if that
+     * slot if empty; otherwise, if the slot has data in it, then sets
+     * the current game state to the state stored at that slot. When saving,
+     * this method also records the timestamp of the save time to the
+     * game's saves location.
+     *
+     * @param {number} slot_number
+     */
+    saveLoadSlot(slot_number)
+    {
+        slot_number = parseInt(slot_number);
+        let saves_location = game.locations['saves'];
+        let game_state = localStorage.getItem("slot" + game.id + slot_number);
+        if (game_state) {
+            this.clearHistory();
+            sessionStorage["current" + game.id] = game_state;
+            this.restoreState(game_state);
+        } else {
+            let save_state = this.captureState();
+            game_state = this.history[this.history.length - 1];
+            this.restoreState(game_state);
+            saves_location['filename' + slot_number] =  this.timestamp;
+            localStorage.setItem("slot" + game.id + slot_number, game_state);
+            this.restoreState(save_state);
+            this.evaluateAction(saves_location['default-action']);
+        }
+    }
+    /**
+     * Deletes any game data from localStorage at location
+     * "slot" + slot_number.
+     *
+     * @param {number} slot_number which save game to delete. Games are stored
+     *   at a localStorage field "slot" + slot_number where it is intended
+     *   (but not enforced) that the slot_number be an integer.
+     */
+    deleteSlotData(slot_number)
+    {
+        slot_number = parseInt(slot_number);
+        let saves_location = game.locations['saves'];
+        localStorage.removeItem("slot" + game.id + slot_number);
+        saves_location['filled' + slot_number] = "not-filled";
+        saves_location['delete' + slot_number] = "disabled";
+        saves_location['slot' + slot_number] =
+            tl[locale]['init_slot_states_save'];
+        saves_location['filename' + slot_number] =  "...";
+    }
+    /**
+     * Deletes any game data from localStorage at location
+     * "slot" + slot_number, updates the game's saves location to reflect the
+     * change.
+     *
+     * @param {number} slot_number which save game to delete. Games are stored
+     *   at a localStorage field "slot" + slot_number where it is intended
+     *   (but not enforced) that the slot_number be an integer.
+     */
+    deleteSlot(slot_number)
+    {
+        this.deleteSlotData(slot_number);
+        let saves_location = game.locations['saves'];
+        this.evaluateAction(saves_location['default-action']);
+    }
+    /**
+     * Deletes all game saves from sessionStorage
+     */
+    deleteSlotAll()
+    {
+        let i = 0;
+        let saves_location = game.locations['saves'];
+        while (saves_location.hasOwnProperty('filename' + i)) {
+            this.deleteSlotData(i);
+            i++;
+        }
+        this.evaluateAction(saves_location['default-action']);
+    }
+    /**
+     * Launches a file picker to allow the user to select a file
+     * containing a saved game state, then tries to load the current game
+     * from this file.
+     */
+    load()
+    {
+        let file_load = elt('file-load');
+        if (!file_load) {
+            file_load = document.createElement("input");
+            file_load.type = "file";
+            file_load.id = 'file-load';
+            file_load.style.display = 'none';
+            file_load.addEventListener('change', (event) => {
+                let to_read = file_load.files[0];
+                let file_reader = new FileReader();
+                file_reader.readAsText(to_read, 'UTF-8')
+                file_reader.addEventListener('load', (load_event) => {
+                    let game_state = load_event.target.result;
+                    this.clearHistory();
+                    sessionStorage["current" + this.id] = game_state;
+                    this.restoreState(game_state);
+                    game.describeMainCharacterLocation();
+                });
+            });
+        }
+        file_load.click();
+    }
+    /**
+     * Creates a downloadable save file for the current game state.
+     */
+    save()
+    {
+        let game_state = this.history[this.history.length - 1];
+        let file = new Blob([game_state], {type: "plain/text"});
+        let link = document.createElement("a");
+        link.href = URL.createObjectURL(file);
+        link.download = "game_save.txt";
+        link.click();
+    }
+    /**
+     * Computes one turn of the current game based on the provided url hash
+     * fragment. A url hash fragment is the part of the url after a # symbol.
+     * In non-game HTML, #fragment is traditionally used to indicate the browser
+     * should show the page as if it had been scrolled to where the element
+     * with id attribute fragment is. In a FRISE game, a fragment has
+     * the form #action_1_name;action_2_name;...;action_n_name;next_location_id
+     * Such a fragment when processed by takeTurn will cause the Javascript in
+     * x-action tags with id's action_1_name, action_2_name,...,action_n_name
+     * to be invoked in turn. Then the main-character object is moved to
+     * location next_location_id.  If the fragment, only consists of
+     * 1 item, i.e., is of the form, #next_location_id, then this method
+     * just moves the main-character to next_location_id.
+     * After carrying out the action and moving the main-character,
+     * takeTurn updates the game state history and future_history
+     * accordingly. Then for each object and each location,
+     * if the object/location, has an x-default-action tag, this default action
+     * is executed. Finally, the Location of the main-character is presented
+     * (its renderPresentation is called).
+     * takeTurn supports two special case action #previous and #next
+     * which move one step back or forward (if possible) in the Game state
+     * history.
+     * @param {string} hash url fragment ot use when computing one turn of the
+     *  current game.
+     */
+    takeTurn(hash)
+    {
+        let new_game_state;
+        let is_slides = sel("x-slides")[0] ? true : false;
+        if (!is_slides) {
+            if (this.has_nav_bar) {
+                if (hash == "#previous") {
+                    this.previousHistory();
+                    return;
+                } else if (hash == "#next") {
+                    this.nextHistory();
+                    return;
+                }
+            }
+            if (sessionStorage["current" + game.id]) {
+                new_game_state = sessionStorage["current" + game.id];
+            }
+        }
+        if (!this.moveMainCharacter(hash)) {
+            return;
+        }
+        if (!is_slides) {
+            this.future_history = [];
+            if (this.has_nav_bar) {
+                elt('next-history').disabled = true;
+            }
+            if (sessionStorage["current" + game.id]) {
+                this.history.push(new_game_state);
+            }
+            this.evaluateDefaultActions(this.objects);
+            this.evaluateDefaultActions(this.locations);
+            sessionStorage["current" + game.id] = this.captureState();
+        }
+        this.describeMainCharacterLocation();
+        if (!is_slides) {
+            game.reload = false;
+            if (this.has_nav_bar) {
+                if (this.history.length == 0) {
+                    elt('previous-history').disabled = true;
+                } else {
+                    elt('previous-history').disabled = false;
+                }
+            }
+        }
+    }
+    /**
+     * For each game Object and each game Location in x_entities evaluate the
+     * Javascript (if it exists) of its default action (from its
+     * x-default-action tag).
+     *
+     * @param {Array} of game Object's or Location's
+     */
+    evaluateDefaultActions(x_entities)
+    {
+        for (const object_name in x_entities) {
+            let game_entity = x_entities[object_name];
+            if (mc().position == object_name && game_entity
+                instanceof Location) {
+                game['is_here'] = true;
+            } else {
+                game['is_here'] = false;
+            }
+            if (game_entity && game_entity['default-action']) {
+                this.evaluateAction(game_entity['default-action']);
+            }
+        }
+    }
+    /**
+     * Moves a game Object to a new game Location. If the object had a
+     * previous location, then also deletes the object from there.
+     *
+     * @param {string} object_id of game Object to move
+     * @param {string} destination_id of game Location to move it to
+     */
+    moveObject(object_id, destination_id)
+    {
+        let move_object = this.objects[object_id];
+        if (!move_object || !this.locations[destination_id]) {
+            alert(tl[locale]['move_object_failed'] +
+                "\nmoveObject('" + object_id + "', '" + destination_id + "')");
+            return false;
+        }
+        if (move_object.hasOwnProperty("position")) {
+            let old_position = move_object.position;
+            let old_location = this.locations[old_position];
+            old_location.items = old_location.items.filter((value) => {
+                return value != object_id;
+            });
+        }
+        move_object.position = destination_id;
+        let new_location = this.locations[destination_id];
+        if (!new_location.items) {
+            new_location.items = [];
+        }
+        new_location.items.push(object_id);
+        return true;
+    }
+    /**
+     * Moves the main character according to the provided url fragment.
+     *
+     * @param {string} hash a url fragment as described above
+     */
+    moveMainCharacter(hash)
+    {
+        if (!hash || hash <= 1) {
+            return true;
+        }
+        hash = hash.substring(1);
+        let hash_parts = hash.split(/\s*\;\s*/);
+        let destination = hash_parts.pop();
+        for (const hash_part of hash_parts) {
+            let hash_matches = hash_part.match(/([^\(]+)(\(([^\)]+)\))?\s*/);
+            let action = elt(hash_matches[1]);
+            let args = [];
+            if (typeof hash_matches[3] !== 'undefined') {
+                args = hash_matches[3].split(",");
+            }
+            if (action && action.tagName == 'X-ACTION'
+                || (action.tagName == 'SCRIPT' &&
+                    action.getAttribute('type') == 'text/action')) {
+                let code = action.innerHTML;
+                if (code) {
+                    this.evaluateAction(code, args);
+                }
+            }
+        }
+        if (destination == "exit") {
+            this.describeMainCharacterLocation();
+            return false;
+        }
+        if (destination == "previous") {
+            this.previousHistory();
+            return false;
+        }
+        if (destination == "next") {
+            this.nextHistory();
+            return false;
+        }
+        let mc = obj('main-character');
+        if (mc.position != mc.destination && mc.position != 'saves') {
+            mc.old_position = mc.position;
+        }
+        if (this.dollhouse_update_ids.length > 0) {
+            for (const update_id of this.dollhouse_update_ids) {
+                clearTimeout(update_id);
+            }
+            this.dollhouse_update_ids = [];
+        }
+        this.moveObject('main-character', destination);
+        this.locations[mc.position].visited++;
+        return true;
+    }
+    /**
+     * Given a string holding pre-Javascript code from an x-action tag,
+     * evaluates the code. If this function  is passed additional arguments
+     * then an args array is set up that can be used as a closure variable for
+     * this eval call.
+     *
+     * @param {string} Javascript code.
+     */
+    evaluateAction(code)
+    {
+        var args = [];
+        if (arguments.length > 1) {
+            if (arguments[1]) {
+                args = arguments[1];
+            }
+        }
+        eval(code);
+    }
+    /**
+     * Used to present the location that the Main Character is currently at.
+     */
+    describeMainCharacterLocation()
+    {
+        let main_character = this.objects['main-character'];
+        let position = main_character.position;
+        let location = this.locations[position];
+        location.renderPresentation();
+    }
+    /**
+     * Return the array of link ids which should be disable while performing
+     * the staging of a presentation
+     *
+     * @return {Array}
+     */
+    volatileLinks()
+    {
+        return this.volatile_links;
+    }
+}
+/**
+ * Module initialization function used to set up the game object corresponding
+ * to the current HTML document. It first loads any x-included game fragments
+ * before calling @see finishInitGame to perform the rest of the game
+ * initialization process
+ */
+async function initGame()
+{
+    loadIncludes(finishInitGame);
+}
+/**
+ * This module initialization is called to initialize FRISE when it is being
+ * used to show a slide presentation. It first loads any x-included slideshow
+ * fragments before calling @see finishInitSlides to perform the rest of the
+ * game initialization process
+ */
+async function initSlides()
+{
+    loadIncludes(finishInitSlides);
+}
+/**
+ * Function used to load any x-included game fragments into this game.
+ * This function is run before the rest of the game's initialization is
+ * done in @see finishInitGame or finishInitSlides
+ *
+ * @param {Function} callback called to complete game initialization after
+ *      the included game fragments are laoded. This is typically either
+ *      finishInitGame or finishInitSlides
+ */
+async function loadIncludes(callback)
+{
+    let includes = sel("x-include");
+    includes_to_load = includes.length;
+    if (includes_to_load == 0) {
+        return callback();
+    }
+    for (const include_node of includes) {
+        if (include_node.nextSibling.nodeName == 'X-LOADED') {
+            includes_to_load--;
+            continue;
+        }
+        let url = include_node.textContent.trim();
+        fetch(url).then(response => response.text()).then(
+            data => {
+                let loaded_node = document.createElement('x-loaded');
+                loaded_node.innerHTML = data;
+                include_node.parentNode.insertBefore(loaded_node,
+                    include_node.nextSibling);
+                includes_to_load --;
+                if (includes_to_load <= 0) {
+                    if (window.location.search == "?view-source") {
+                        let encodeEntities = (text) => {
+                            let textArea = document.createElement('textarea');
+                            textArea.innerText = text;
+                            return textArea.innerHTML;
+                        }
+                        let source = "<!doctype html>\n" +
+                            document.documentElement.outerHTML;
+                        source = encodeEntities(source);
+                        let body = sel('body')[0];
+                        body.innerHTML = `<pre><code>${source}</pre></code>`;
+                    } else {
+                        callback();
+                    }
+                }
+            }
+        );
+    }
+    if (includes_to_load <= 0) {
+        if (window.location.search == "?view-source") {
+            alert("<!DOCTYPE html>\n" + document.documentElement.outerHTML);
+        } else {
+            callback();
+        }
+    }
+}
+/**
+ * This function is called after all images for Doll's have
+ * loaded to finish initializing the Frise Game. To do this, if there is a
+ * current game state in sessionStorage it is used to initialize the game
+ * state, otherwise,* the game state is based on the start of the game. After
+ * this state is set up, the current location is drawn to the game content area.
+ */
+async function finishInitGame()
+{
+    game = new Game();
+    /*
+      Any game specific customizations are assumed to be in the function
+      localInitGame if it exists
+     */
+    if (typeof localInitGame == 'function') {
+        localInitGame();
+    }
+    let use_session = false;
+    let is_slides = sel("x-slides")[0] ? true : false;
+    if (sessionStorage["current" + game.id] && !is_slides) {
+        use_session = true;
+        game.restoreState(sessionStorage["current" + game.id]);
+        game.reload = true;
+    } else {
+        game.base_location = game.objects['main-character'].position;
+    }
+    if (is_slides) {
+        window.addEventListener('hashchange', (event) => {
+            game.takeTurn(window.location.hash);
+        })
+    }
+    game.takeTurn("");
+    game.clearHistory();
+    game.initializeScreen();
+}
+/**
+ * If the current game is a slideshow presentation
+ * this function is called after all images for Doll's have
+ * loaded to finish initializing the slides for the presentation. This
+ * sets up the forward, backward, table of conents, all slides on a single
+ * page, and reset buttons as well as keyboard listeners to go this through
+ * slides
+ */
+async function finishInitSlides()
+{
+    let slides = sel('x-slides')[0];
+    let home = sel(`link[rel=prev], meta[name=parent],
+        meta[property=parent]`)[0] ?? "";
+    if (home) {
+        home = home.getAttribute('content') ??
+            home.getAttribute('href');
+        if (home) {
+            home = `<x-button href='${home}'>🏠</x-button>`;
+        }
+    }
+    let slides_in_transition = slides.getAttribute("in-transition");
+    let slides_out_transition = slides.getAttribute("out-transition");
+    let slides_duration = slides.getAttribute("duration");
+    slides_duration = (slides_duration) ? slides_duration : "500";
+    let slides_iterations = slides.getAttribute("iteration");
+    let slides_origin = slides.getAttribute("origin");
+    let slides_children = slides.children;
+    if (!slides_children) {
+        return;
+    }
+    let slide_titles = sel('x-slides x-slide h1:first-of-type');
+    let x_game = sel('x-game')[0];
+    if (!x_game) {
+        x_game = document.createElement('x-game');
+        let body = sel('body')[0];
+        if (!body) {
+            return;
+        }
+        body.appendChild(x_game);
+    }
+    let i = 1;
+    let num_slides = 0;
+    let start_slide = "";
+    let all_slides = "<x-present><div class='all'>";
+    let title_location = `<x-location id="(titles)"><x-present>
+        <h1>${tl['en']['slide_titles']}</h1><ol style="font-size:130%;">`;
+    let game_contents = `<x-object id="main-character">
+       <x-position>(1)</x-position></x-object>`;
+    for (const slide of slides_children) {
+        if (slide.tagName == 'X-SLIDE') {
+            num_slides++;
+        }
+    }
+    for (const slide_title of slide_titles) {
+        title_location += `<li><a href='#(${i})'
+            >${slide_title.textContent}</a></li>`;
+        i++;
+    }
+    let title_slide_footer = ` <span class="big-p">(</span>
+        <x-button href="#(1)">⟳</x-button>
+        <x-button class="slides-titles" href="#(1)">-/${num_slides}</x-button>
+        <x-button href="#(all)">≡</x-button>
+        <span class="big-p">)</span> `;
+    title_location += `</ol></x-present><x-screen-footer
+        ><div class='slide-footer center'
+        >${title_slide_footer}</div></x-screen-footer></x-location>`;
+    game_contents += title_location;
+    i = 1;
+    let slide_footer, all_slide_footer;
+    for (const slide of slides_children) {
+        if (slide.tagName == 'X-SLIDE') {
+            let in_transition = slide.getAttribute('in-transition');
+            in_transition ??= (slides_in_transition ?? "");
+            let out_transition = slide.getAttribute('out-transition');
+            out_transition ?? (out_transition ?? slides_out_transition);
+            let all_class_attribute = (slide.className) ?
+                ` class='slide ${slide.className}' `: ` class='slide' `;
+            let class_attribute = (slide.className) ?
+                ` class='slide ${slide.className} ${in_transition}' `:
+                ` class='slide ${in_transition}' `;
+            let styles = slide.getAttribute('style');
+            let style_attribute = (styles) ? ` style='${styles}' ` : "";
+            let duration = slide.getAttribute('duration');
+            duration = (duration) ? duration : slides_duration;
+            let iterations = slide.getAttribute('iterations');
+            iterations = (iterations) ? iterations : slides_iterations;
+            let origin = slide.getAttribute('origin');
+            origin = (origin) ? origin : slides_origin;
+            if (out_transition) {
+                out_transition += ` data-transition="${out_transition}" `;
+            } else {
+                out_transition = "";
+            }
+            if (duration) {
+                out_transition += ` data-duration="${duration}" `;
+            }
+            if (iterations) {
+                out_transition += ` data-iterations="${iterations}" `;
+            }
+            if (origin) {
+                out_transition += ` data-transform-origin="${origin}" `;
+            }
+            let slide_contents = `<div ${class_attribute} ${style_attribute}>
+                ${slide.innerHTML}</div>`;
+            let current_slide = `<x-present>${slide_contents}`;
+            let all_slide_contents = `<div ${all_class_attribute}
+                ${style_attribute}>${slide.innerHTML}</div>`;
+            if (i > 1) {
+                slide_footer = `<x-button class="slides-previous"
+                    href="#(${i - 1})" ${out_transition}>←</x-button>`;
+            } else {
+                slide_footer = `<x-button class="hidden">←</x-button>`
+            }
+            slide_footer += ` <span class="big-p">(</span>
+                <x-button class="slides-restart" href="#(1)">⟳</x-button>
+                ${home} <x-button class="slides-titles" href="#(titles)"
+                    >${i}/${num_slides}</x-button>
+                <x-button class="all-slides" href="#(all)">≡</x-button>
+                <span class="big-p">)</span>`;
+            if (i < num_slides) {
+                slide_footer += `<x-button class="slides-next"
+                    href="#(${i + 1})" ${out_transition}>→</x-button>`
+            } else {
+                slide_footer += `<x-button class="hidden">→</x-button>`
+            }
+            all_slides += all_slide_contents;
+            if (i < num_slides) {
+                all_slides +=`<div class="page-break"><hr></div>`;
+            }
+            current_slide += `</x-present>`;
+            game_contents += `<x-location id="(${i})">
+                ${current_slide}<x-screen-footer>
+                    <div class='slide-footer center'
+                    >${slide_footer}</div></x-screen-footer>
+                </x-location>`;
+            i++;
+        }
+    }
+    all_slide_footer = `<div class='slide-footer center'>
+        <span class="big-p">(</span>
+        <x-button class="slides-restart" href="#(1)">⟳</x-button>
+        <x-button class="slides-titles" href="#(titles)"
+            >-/${num_slides}</x-button>
+        <x-button class="all-slides" href="#(1)">≡</x-button>
+        <span class="big-p">)</span></div>`;
+    all_slides += `</div></x-present>`;
+    game_contents += `<x-location id="(all)">
+        ${all_slides}<x-screen-footer>
+        <div class='slide-footer center'
+            >${all_slide_footer}</div></x-screen-footer>
+        </x-location>`;
+    x_game.innerHTML = game_contents;
+    let body = sel('body')[0];
+    let start_x = 0;
+    let start_swipe_ck = (e) => {
+        start_x = (e.changedTouches ? e.changedTouches[0] : e).clientX;
+    };
+    let end_swipe_ck = (e) => {
+        let dx = (e.changedTouches ? e.changedTouches[0] : e).clientX -
+            start_x;
+        if (dx > 30) {
+            let previous = sel('#screen-footer .slides-previous')[0];
+            if (previous) {
+                previous.click();
+            }
+        } else if (dx < -30 ) {
+            let next = sel('#screen-footer .slides-next')[0];
+            if (next) {
+                next.click();
+            }
+        }
+    }
+    body.addEventListener('mousedown', start_swipe_ck, false);
+    body.addEventListener('touchstart', start_swipe_ck, false);
+    body.addEventListener('mouseup', end_swipe_ck, false);
+    body.addEventListener('touchend', end_swipe_ck, false);
+    body.addEventListener('keydown', (event) => {
+        let hash = window.location.hash ?? 1;
+        let page_num = parseInt(hash.substring(2, hash.length - 1));
+        page_num = (isNaN(page_num)) ? 1 : page_num;
+        if (['ArrowLeft'].includes(event.code) &&  page_num > 1) {
+            let previous = sel('#screen-footer .slides-previous')[0];
+            if (previous) {
+                previous.click();
+            }
+        } else  if (['Space', 'ArrowRight', 'Enter'].includes(event.code)
+            && page_num < num_slides) {
+                let next = sel('#screen-footer .slides-next')[0];
+                if (next) {
+                    next.click();
+                }
+        } else if (['KeyT'].includes(event.code)) {
+            let titles = sel('#screen-footer .slides-titles')[0];
+            if (titles) {
+                titles.click();
+            }
+        } else if (['KeyF', 'KeyH'].includes(event.code)) {
+            let slide_footer = sel('#game-screen .slide-footer')[0];
+            if (slide_footer.style.display != 'none') {
+                slide_footer.style.display = 'none';
+                sessionStorage['show_footer'] = false;
+            } else {
+                slide_footer.style.display = 'block';
+                sessionStorage['show_footer'] = true;
+            }
+        } else if (event.code == 'KeyR' || event.code == 'Digit1') {
+            window.location = `#(1)`;
+        } else if (event.code == 'KeyA') {
+            if (mc().position == '(all)') {
+                window.location = "#(1)";
+            } else {
+                window.location = "#(all)";
+            }
+        }
+    });
+    finishInitGame();
+    if (window.location.hash) {
+        game.takeTurn(window.location.hash);
+    }
+}
+window.addEventListener("load", (event) => {
+    let has_onload = sel("body")[0].getAttribute("onload") ? true : false;
+    let is_slides = sel("x-slides")[0] ? true : false;
+    let is_game = sel("x-game")[0] ? true : false;
+    if (has_onload) {
+        return;
+    } else if (is_slides) {
+        initSlides();
+    } else if (is_game) {
+        initGame();
+    }
+});
diff --git a/src/scripts/slidy.js b/src/scripts/slidy.js
deleted file mode 100644
index 0deff54f2..000000000
--- a/src/scripts/slidy.js
+++ /dev/null
@@ -1,2386 +0,0 @@
-/* slidy.js
-    Copyright (c) 2005-2013 W3C (MIT, ERCIM, Keio), All Rights Reserved.
-    W3C liability, trademark, document use and software licensing
-    rules apply, see:
-    http://www.w3.org/Consortium/Legal/copyright-documents
-    http://www.w3.org/Consortium/Legal/copyright-software
-    Defines single name "w3c_slidy" in global namespace
-    Adds event handlers without trampling on any others
-*/
-// the slidy object implementation
-var w3c_slidy = {
-    // classify which kind of browser we're running under
-    ns_pos: (typeof window.pageYOffset!='undefined'),
-    khtml: ((navigator.userAgent).indexOf("KHTML") >= 0 ? true : false),
-    opera: ((navigator.userAgent).indexOf("Opera") >= 0 ? true : false),
-    ipad: ((navigator.userAgent).indexOf("iPad") >= 0 ? true : false),
-    iphone: ((navigator.userAgent).indexOf("iPhone") >= 0 ? true : false),
-    android: ((navigator.userAgent).indexOf("Android") >= 0 ? true : false),
-    ie: (typeof document.all != "undefined" && !this.opera),
-    // data for swipe and double tap detection on touch screens
-    last_tap: 0,
-    prev_tap: 0,
-    start_x: 0,
-    start_y: 0,
-    delta_x: 0,
-    delta_y: 0,
-    // are we running as XHTML? (doesn't work on Opera)
-    is_xhtml: /xml/.test(document.contentType),
-    slide_number: 0, // integer slide count: 0, 1, 2, ...
-    slide_number_element: null, // element containing slide number
-    slides: [], // set to array of slide div's
-    notes: [], // set to array of handout div's
-    backgrounds: [], // set to array of background div's
-    observers: [], // list of observer functions
-    toolbar: null, // element containing toolbar
-    title: null, // document title
-    last_shown: null, // last incrementally shown item
-    eos: null,  // span element for end of slide indicator
-    toc: null, // table of contents
-    outline: null, // outline element with the focus
-    selected_text_len: 0, // length of drag selection on document
-    view_all: 0,  // 1 to view all slides + handouts
-    want_toolbar: true,  // user preference to show/hide toolbar
-    mouse_click_enabled: true, // enables left click for next slide
-    scroll_hack: 0, // IE work around for position: fixed
-    disable_slide_click: false,  // used by clicked anchors
-    lang: "en", // updated to language specified by html file
-    help_anchor: null, // used for keyboard focus hack in showToolbar()
-    help_page: "http://www.w3.org/Talks/Tools/Slidy2/help/help.html",
-    help_text: "Navigate with mouse click, space bar, Cursor Left/Right, " +
-        "or Pg Up and Pg Dn. Use S and B to change font size.",
-    size_index: 0,
-    size_adjustment: 0,
-    sizes: new Array("10pt", "12pt", "14pt", "16pt", "18pt", "20pt",
-        "22pt", "24pt", "26pt", "28pt", "30pt", "32pt"),
-    // needed for efficient resizing
-    last_width: 0,
-    last_height: 0,
-    // Needed for cross browser support for relative width/height on
-    // object elements. The work around is to save width/height attributes
-    // and then to recompute absolute width/height dimensions on resizing
-    objects: [],
-    up_link: null,
-    // attach initialiation event handlers
-    set_up: function () {
-        var init = function() {
-            w3c_slidy.init();
-        };
-        if (typeof window.addEventListener != "undefined") {
-            window.addEventListener("load", init, false);
-        } else {
-            window.attachEvent("onload", init);
-        }
-    },
-    hide_slides: function () {
-        if (this.body && !w3c_slidy.initialized) {
-            this.body.style.visibility = "hidden";
-        } else {
-            setTimeout(w3c_slidy.hide_slides, 50);
-        }
-    },
-    // hack to persuade IE to compute correct document height
-    // as needed for simulating fixed positioning of toolbar
-    ie_hack: function () {
-        window.resizeBy(0,-1);
-        window.resizeBy(0, 1);
-    },
-    init: function () {
-        this.body = document.body;
-        if (typeof setDisplay === "function") {
-            setDisplay("top-container", false);
-        }
-        this.body.style.visibility = "visible";
-        this.init_localization();
-        this.add_toolbar();
-        this.wrap_implicit_slides();
-        this.collect_slides();
-        this.collect_notes();
-        this.collect_backgrounds();
-        this.objects = this.body.getElementsByTagName("object");
-        this.patch_anchors();
-        this.slide_number = this.find_slide_number(location.href);
-        window.offscreenbuffering = true;
-        this.size_adjustment = this.find_size_adjust();
-        this.time_left = this.find_duration();
-        this.hide_image_toolbar();  // suppress IE image toolbar popup
-        this.init_outliner();  // activate fold/unfold support
-        this.title = document.title;
-        this.keyboardless = (this.ipad||this.iphone||this.android);
-        if (this.keyboardless) {
-            w3c_slidy.remove_class(w3c_slidy.toolbar, "hidden")
-            this.want_toolbar = 0;
-        }
-        // work around for opera bug
-        this.is_xhtml = (this.body.tagName == "BODY" ? false : true);
-        if (this.slides.length > 0) {
-            var slide = this.slides[this.slide_number];
-            if (this.slide_number > 0) {
-                this.set_visibility_all_incremental("visible");
-                this.last_shown = this.previous_incremental_item(null);
-                this.set_eos_status(true);
-            } else {
-                this.last_shown = null;
-                this.set_visibility_all_incremental("hidden");
-                this.set_eos_status(
-                    !this.next_incremental_item(this.last_shown));
-            }
-            this.set_location();
-            this.add_class(this.slides[0], "first-slide");
-            w3c_slidy.show_slide(slide);
-        }
-        this.toc = this.table_of_contents();
-        this.add_initial_prompt();
-        // bind event handlers without interfering with custom page scripts
-        // Tap events behave too weirdly to support clicks reliably on
-        // iPhone and iPad, so exclude these from click handler
-        if (!this.keyboardless) {
-            this.add_listener(this.body, "click", this.mouse_button_click);
-            this.add_listener(this.body, "mousedown",
-                this.mouse_button_down);
-        }
-        this.add_listener(document, "keydown", this.key_down);
-        this.add_listener(document, "keypress", this.key_press);
-        this.add_listener(window, "resize", this.resized);
-        this.add_listener(window, "scroll", this.scrolled);
-        this.add_listener(window, "unload", this.unloaded);
-        this.add_listener(document, "gesturechange", function () {
-            return false;
-        });
-        this.attach_touch_handers(this.slides);
-        this.single_slide_view();
-        this.resized();
-        this.show_toolbar();
-        // for back button detection
-        setInterval(function () {
-            w3c_slidy.check_location();
-        }, 200);
-        w3c_slidy.initialized = true;
-    },
-    // create div element with links to each slide
-    table_of_contents: function () {
-        var toc = this.create_element("div");
-        this.add_class(toc, "slidy_toc hidden");
-        //toc.setAttribute("tabindex", "0");
-        var heading = this.create_element("div");
-        this.add_class(heading, "toc-heading");
-        heading.innerHTML = this.localize("Table of Contents");
-        toc.appendChild(heading);
-        var previous = null;
-        for (var i = 0; i < this.slides.length; ++i) {
-            var title = this.has_class(this.slides[i], "title");
-            var num = document.createTextNode((i + 1) + ". ");
-            toc.appendChild(num);
-            var a = this.create_element("a");
-            a.setAttribute("href", "#(" + (i+1) + ")");
-            if (title) {
-                this.add_class(a, "titleslide");
-            }
-            var name = document.createTextNode(this.slide_name(i));
-            a.appendChild(name);
-            a.onclick = w3c_slidy.toc_click;
-            a.onkeydown = w3c_slidy.toc_key_down;
-            a.previous = previous;
-            if (previous) {
-                previous.next = a;
-            }
-            toc.appendChild(a);
-            if (i == 0) {
-                toc.first = a;
-            }
-            if (i < this.slides.length - 1) {
-                var br = this.create_element("br");
-                toc.appendChild(br);
-            }
-            previous = a;
-        }
-        toc.focus = function () {
-            if (this.first) {
-                this.first.focus();
-            }
-        }
-        toc.onmouseup = w3c_slidy.mouse_button_up;
-        toc.onclick = function (e) {
-            e||(e=window.event);
-            if (w3c_slidy.selected_text_len <= 0) {
-                w3c_slidy.hide_table_of_contents(true);
-            }
-            w3c_slidy.stop_propagation(e);
-            if (e.cancel != undefined) {
-                e.cancel = true;
-            }
-            if (e.returnValue != undefined) {
-                e.returnValue = false;
-            }
-            return false;
-        };
-        this.body.insertBefore(toc, this.body.firstChild);
-        return toc;
-    },
-    is_shown_toc: function () {
-        return !w3c_slidy.has_class(w3c_slidy.toc, "hidden");
-    },
-    show_table_of_contents: function () {
-        w3c_slidy.remove_class(w3c_slidy.toc, "hidden");
-        var toc = w3c_slidy.toc;
-        toc.focus();
-    },
-    hide_table_of_contents: function (focus) {
-        w3c_slidy.add_class(w3c_slidy.toc, "hidden");
-        if (focus && !w3c_slidy.opera &&
-            !w3c_slidy.has_class(w3c_slidy.toc, "hidden")) {
-            w3c_slidy.set_focus();
-        }
-    },
-    toggle_table_of_contents: function () {
-        if (w3c_slidy.is_shown_toc()) {
-            w3c_slidy.hide_table_of_contents(true);
-        } else {
-            w3c_slidy.show_table_of_contents();
-        }
-    },
-    // called on clicking toc entry
-    toc_click: function (e) {
-        if (!e) {
-            e = window.event;
-        }
-        var target = w3c_slidy.get_target(e);
-        if (target && target.nodeType == 1) {
-            var uri = target.getAttribute("href");
-            if (uri) {
-                var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.hide_slide(slide);
-                w3c_slidy.slide_number = w3c_slidy.find_slide_number(uri);
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.last_shown = null;
-                w3c_slidy.set_location();
-                w3c_slidy.set_visibility_all_incremental("hidden");
-                w3c_slidy.set_eos_status(
-                    !w3c_slidy.next_incremental_item(w3c_slidy.last_shown));
-                w3c_slidy.show_slide(slide);
-                try {
-                    if (!w3c_slidy.opera) {
-                        w3c_slidy.set_focus();
-                    }
-                } catch (e) {
-                }
-            }
-        }
-        w3c_slidy.hide_table_of_contents(true);
-        w3c_slidy.stop_propagation(e);
-        return w3c_slidy.cancel(e);
-    },
-    // called onkeydown for toc entry
-    toc_key_down: function (event) {
-        var key;
-        if (!event) {
-            var event = window.event;
-        }
-        // kludge around NS/IE differences
-        if (window.event) {
-            key = window.event.keyCode;
-        } else if (event.which) {
-            key = event.which;
-        } else {
-            return true; // Yikes! unknown browser
-        }
-        // ignore event if key value is zero
-        // as for alt on Opera and Konqueror
-        if (!key) {
-            return true;
-        }
-        // check for concurrent control/command/alt key
-        // but are these only present on mouse events?
-        if (event.ctrlKey || event.altKey) {
-            return true;
-        }
-        if (key == 13) {
-            var uri = this.getAttribute("href");
-            if (uri) {
-                var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.hide_slide(slide);
-                w3c_slidy.slide_number = w3c_slidy.find_slide_number(uri);
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.last_shown = null;
-                w3c_slidy.set_location();
-                w3c_slidy.set_visibility_all_incremental("hidden");
-                w3c_slidy.set_eos_status(!w3c_slidy.next_incremental_item(
-                    w3c_slidy.last_shown));
-                w3c_slidy.show_slide(slide);
-                try {
-                    if (!w3c_slidy.opera) {
-                        w3c_slidy.set_focus();
-                    }
-                } catch (e) {
-                }
-            }
-            w3c_slidy.hide_table_of_contents(true);
-            return w3c_slidy.cancel(event);
-        }
-        if (key == 40 && this.next) {
-            this.next.focus();
-            return w3c_slidy.cancel(event);
-        }
-        if (key == 38 && this.previous) {
-            this.previous.focus();
-            return w3c_slidy.cancel(event);
-        }
-        return true;
-    },
-    touchstart: function (e) {
-        // a double touch often starts with a
-        // single touch due to fingers touching
-        // down at slightly different times
-        // thus avoid calling preventDefault here
-        this.prev_tap = this.last_tap;
-        this.last_tap = (new Date).getTime();
-        var tap_delay = this.last_tap - this.prev_tap;
-        if (tap_delay <= 200) {
-            // double tap
-        }
-        var touch = e.touches[0];
-        this.pageX = touch.pageX;
-        this.pageY = touch.pageY;
-        this.screenX = touch.screenX;
-        this.screenY = touch.screenY;
-        this.clientX = touch.clientX;
-        this.clientY = touch.clientY;
-        this.delta_x = this.delta_y = 0;
-    },
-    touchmove: function (e)
-    {
-        // override native gestures for single touch
-        if (e.touches.length > 1) {
-            return;
-        }
-        e.preventDefault();
-        var touch = e.touches[0];
-        this.delta_x = touch.pageX - this.pageX;
-        this.delta_y = touch.pageY - this.pageY;
-    },
-    touchend: function (e) {
-        // default behavior for multi-touch
-        if (e.touches.length > 1) {
-            return;
-        }
-        var delay = (new Date).getTime() - this.last_tap;
-        var dx = this.delta_x;
-        var dy = this.delta_y;
-        var abs_dx = Math.abs(dx);
-        var abs_dy = Math.abs(dy);
-        if (delay < 500 && (abs_dx > 100 || abs_dy > 100)) {
-            if (abs_dx > 0.5 * abs_dy) {
-                e.preventDefault();
-                if (dx < 0) {
-                    w3c_slidy.next_slide(true);
-                } else {
-                    w3c_slidy.previous_slide(true);
-                }
-            } else if (abs_dy > 2 * abs_dx) {
-                e.preventDefault();
-                w3c_slidy.toggle_table_of_contents();
-            }
-        }
-    },
-    // ### OBSOLETE ###
-    before_print: function () {
-        this.show_all_slides();
-        this.hide_toolbar();
-        alert("before print");
-    },
-    // ### OBSOLETE ###
-    after_print: function () {
-        if (!this.view_all) {
-            this.single_slide_view();
-            this.show_toolbar();
-        }
-        alert("after print");
-    },
-    // ### OBSOLETE ###
-    print_slides: function () {
-        this.before_print();
-        window.print();
-        this.after_print();
-    },
-    // ### OBSOLETE ?? ###
-    toggle_view: function () {
-        if (this.view_all) {
-            this.single_slide_view();
-            this.show_toolbar();
-            this.view_all = 0;
-        } else {
-            this.show_all_slides();
-            this.hide_toolbar();
-            this.view_all = 1;
-        }
-    },
-    // prepare for printing  ### OBSOLETE ###
-    show_all_slides: function () {
-        this.remove_class(this.body, "single_slide");
-        this.set_visibility_all_incremental("visible");
-    },
-    // restore after printing  ### OBSOLETE ###
-    single_slide_view: function () {
-        this.add_class(this.body, "single_slide");
-        this.set_visibility_all_incremental("visible");
-        this.last_shown = this.previous_incremental_item(null);
-    },
-    // suppress IE's image toolbar pop up
-    hide_image_toolbar: function () {
-        if (!this.ns_pos) {
-            var images = document.getElementsByTagName("IMG");
-            for (var i = 0; i < images.length; ++i) {
-                images[i].setAttribute("galleryimg", "no");
-            }
-        }
-    },
-    unloaded: function (e) {
-    },
-    // Safari and Konqueror don't yet support getComputedStyle()
-    // and they always reload page when location.href is updated
-    is_KHTML: function () {
-        var agent = navigator.userAgent;
-        return (agent.indexOf("KHTML") >= 0 ? true : false);
-    },
-    // find slide name from first h1 element
-    // default to document title + slide number
-    slide_name: function (index) {
-        var name = null;
-        var slide = this.slides[index];
-        var heading = this.find_heading(slide);
-        if (heading) {
-            name = this.extract_text(heading);
-        }
-        if (!name) {
-            name = this.title + "(" + (index + 1) + ")";
-        }
-        name.replace(/\&/g, "&amp;");
-        name.replace(/\</g, "&lt;");
-        name.replace(/\>/g, "&gt;");
-        return name;
-    },
-    // find first h1 element in DOM tree
-    find_heading: function (node) {
-        if (!node || node.nodeType != 1) {
-            return null;
-        }
-        if (node.nodeName == "H1" || node.nodeName == "h1") {
-            return node;
-        }
-        var child = node.firstChild;
-        while (child) {
-            node = this.find_heading(child);
-            if (node) {
-                return node;
-            }
-            child = child.nextSibling;
-        }
-        return null;
-    },
-    // recursively extract text from DOM tree
-    extract_text: function (node) {
-        if (!node) {
-          return "";
-        }
-        // text nodes
-        if (node.nodeType == 3) {
-            return node.nodeValue;
-        }
-        // elements
-        if (node.nodeType == 1) {
-            node = node.firstChild;
-            var text = "";
-            while (node) {
-                text = text + this.extract_text(node);
-                node = node.nextSibling;
-            }
-            return text;
-        }
-        return "";
-    },
-    // find copyright text from meta element
-    find_copyright: function () {
-        var name, content;
-        var meta = document.getElementsByTagName("meta");
-        for (var i = 0; i < meta.length; ++i) {
-            name = meta[i].getAttribute("name");
-            content = meta[i].getAttribute("content");
-            if (name == "copyright") {
-                return content;
-            }
-        }
-        return null;
-    },
-    find_size_adjust: function () {
-        var name, content, offset;
-        var meta = document.getElementsByTagName("meta");
-        for (var i = 0; i < meta.length; ++i) {
-            name = meta[i].getAttribute("name");
-            content = meta[i].getAttribute("content");
-            if (name == "font-size-adjustment") {
-                return 1 * content;
-            }
-        }
-        return 1;
-    },
-    // <meta name="duration" content="20" >  for 20 minutes
-    find_duration: function () {
-        var name, content, offset;
-        var meta = document.getElementsByTagName("meta");
-        for (var i = 0; i < meta.length; ++i) {
-            name = meta[i].getAttribute("name");
-            content = meta[i].getAttribute("content");
-            if (name == "duration") {
-                return 60000 * content;
-            }
-        }
-        return null;
-    },
-    replace_by_non_breaking_space: function (str) {
-        for (var i = 0; i < str.length; ++i) {
-            str[i] = 160;
-        }
-    },
-    // ### CHECK ME ### is use of "li" okay for text/html?
-    // for XHTML do we also need to specify namespace?
-    init_outliner: function () {
-        var items = document.getElementsByTagName("li");
-        for (var i = 0; i < items.length; ++i) {
-            var target = items[i];
-            if (!this.has_class(target.parentNode, "outline")) {
-                continue;
-            }
-            target.onclick = this.outline_click;
-            if (this.foldable(target)) {
-                target.foldable = true;
-                target.onfocus = function () {
-                    w3c_slidy.outline = this;
-                };
-                target.onblur = function () {
-                    w3c_slidy.outline = null;
-                };
-                if (!target.getAttribute("tabindex")) {
-                    target.setAttribute("tabindex", "0");
-                }
-                if (this.has_class(target, "expand")) {
-                    this.unfold(target);
-                } else {
-                    this.fold(target);
-                }
-            } else {
-                this.add_class(target, "nofold");
-                target.visible = true;
-                target.foldable = false;
-            }
-        }
-    },
-    foldable: function (item) {
-        if (!item || item.nodeType != 1) {
-            return false;
-        }
-        var node = item.firstChild;
-        while (node) {
-            if (node.nodeType == 1 && this.is_block(node)) {
-                return true;
-            }
-            node = node.nextSibling;
-        }
-        return false;
-    },
-    // ### CHECK ME ### switch to add/remove "hidden" class
-    fold: function (item) {
-        if (item) {
-            this.remove_class(item, "unfolded");
-            this.add_class(item, "folded");
-        }
-        var node = item ? item.firstChild : null;
-        while (node) {
-            if (node.nodeType == 1 && this.is_block(node)) { // element
-                w3c_slidy.add_class(node, "hidden");
-            }
-            node = node.nextSibling;
-        }
-        item.visible = false;
-    },
-    // ### CHECK ME ### switch to add/remove "hidden" class
-    unfold: function (item) {
-        if (item) {
-            this.add_class(item, "unfolded");
-            this.remove_class(item, "folded");
-        }
-        var node = item ? item.firstChild : null;
-        while (node) {
-            if (node.nodeType == 1 && this.is_block(node)) { // element
-                w3c_slidy.remove_class(node, "hidden");
-            }
-            node = node.nextSibling;
-        }
-        item.visible = true;
-    },
-    outline_click: function (e) {
-        if (!e) {
-            e = window.event;
-        }
-        var rightclick = false;
-        var target = w3c_slidy.get_target(e);
-        while (target && target.visible == undefined) {
-            target = target.parentNode;
-        }
-        if (!target) {
-            return true;
-        }
-        if (e.which) {
-            rightclick = (e.which == 3);
-        } else if (e.button) {
-            rightclick = (e.button == 2);
-        }
-        if (!rightclick && target.visible != undefined) {
-            if (target.foldable) {
-                if (target.visible) {
-                    w3c_slidy.fold(target);
-                } else {
-                    w3c_slidy.unfold(target);
-                }
-            }
-            w3c_slidy.stop_propagation(e);
-            e.cancel = true;
-            e.returnValue = false;
-        }
-        return false;
-    },
-    add_initial_prompt: function () {
-        var prompt = this.create_element("div");
-        prompt.setAttribute("class", "initial_prompt");
-        var p1 = this.create_element("p");
-        prompt.appendChild(p1);
-        p1.setAttribute("class", "help");
-        if (this.keyboardless) {
-            p1.innerHTML = "swipe left to move to next slide";
-        } else {
-          p1.innerHTML = "Space, Right Arrow or swipe left to move to " +
-            "next slide, click help below for more details";
-        }
-        this.add_listener(prompt, "click", function (e) {
-            this.body.removeChild(prompt);
-            w3c_slidy.stop_propagation(e);
-            if (e.cancel != undefined) {
-                e.cancel = true;
-            }
-            if (e.returnValue != undefined) {
-                e.returnValue = false;
-            }
-            return false;
-        });
-        this.body.appendChild(prompt);
-        this.initial_prompt = prompt;
-        setTimeout(function() {
-            w3c_slidy.body.removeChild(prompt);
-        }, 5000);
-    },
-    add_toolbar: function () {
-        var counter, page;
-        this.toolbar = this.create_element("div");
-        this.toolbar.setAttribute("class", "toolbar");
-        // a reasonably behaved browser
-        var right = this.create_element("div");
-        right.setAttribute("style", "float: right; text-align: right");
-        counter = this.create_element("span")
-        counter.innerHTML = this.localize("slide") + " n/m";
-        right.appendChild(counter);
-        this.toolbar.appendChild(right);
-        var left = this.create_element("div");
-        left.setAttribute("style", "text-align: left");
-        // global end of slide indicator
-        this.eos = this.create_element("span");
-        this.eos.innerHTML = "* ";
-        left.appendChild(this.eos);
-        var hamburger = this.create_element("a");
-        hamburger.setAttribute("href", "javascript:toggleOptions()");
-        hamburger.innerHTML = "&equiv;";
-        left.appendChild(hamburger);
-        this.spacer = this.create_element("span");
-        this.spacer.innerHTML = "&nbsp; ";
-        left.appendChild(this.spacer);
-        var help = this.create_element("a");
-        help.setAttribute("href", this.help_page);
-        help.setAttribute("title", this.localize(this.help_text));
-        help.innerHTML = this.localize("help?");
-        left.appendChild(help);
-        this.help_anchor = help;  // save for focus hack
-        var gap1 = document.createTextNode(" ");
-        left.appendChild(gap1);
-        var contents = this.create_element("a");
-        contents.setAttribute("href",
-            "javascript:w3c_slidy.toggle_table_of_contents()");
-        contents.setAttribute("title", this.localize("table of contents"));
-        contents.innerHTML = this.localize("contents?");
-        left.appendChild(contents);
-        var gap2 = document.createTextNode(" ");
-        left.appendChild(gap2);
-        var links = document.getElementsByTagName("link");
-        for (var i = 0; i < links.length; i++) {
-            if (links[i].getAttribute("rel") == "prev" ) {
-                w3c_slidy.up_link = links[i].getAttribute("href");
-            }
-        }
-        if (w3c_slidy.up_link) {
-            var contents = this.create_element("a");
-            contents.setAttribute("href", w3c_slidy.up_link);
-            contents.setAttribute("title",
-                this.localize("Pre-slideshow link"));
-            contents.innerHTML = this.localize("up?");
-            left.appendChild(contents);
-        }
-        var copyright = this.find_copyright();
-        if (copyright) {
-            var span = this.create_element("span");
-            span.className = "copyright";
-            span.innerHTML = copyright;
-            left.appendChild(span);
-        }
-        this.toolbar.setAttribute("tabindex", "0");
-        this.toolbar.appendChild(left);
-         // ensure that click isn't passed through to the page
-         this.toolbar.onclick = function (e) {
-            if (!e) {
-                e = window.event;
-            }
-            var target = e.target;
-            if (!target && e.srcElement) {
-                target = e.srcElement;
-            }
-            // work around Safari bug
-            if (target && target.nodeType == 3) {
-                target = target.parentNode;
-            }
-            w3c_slidy.stop_propagation(e);
-            if (target && target.nodeName.toLowerCase() != "a") {
-                w3c_slidy.mouse_button_click(e);
-            }
-        };
-        this.slide_number_element = counter;
-        this.set_eos_status(false);
-        this.body.appendChild(this.toolbar);
-    },
-    // wysiwyg editors make it hard to use div elements
-    // e.g. amaya loses the div when you copy and paste
-    // this function wraps div elements around implicit
-    // slides which start with an h1 element and continue
-    // up to the next heading or div element
-    wrap_implicit_slides: function () {
-        var i, heading, node, next, div;
-        var headings = document.getElementsByTagName("h1");
-        if (!headings) {
-            return;
-        }
-        for (i = 0; i < headings.length; ++i) {
-            heading = headings[i];
-            if (heading.parentNode != this.body) {
-                continue;
-            }
-            node = heading.nextSibling;
-            div = document.createElement("div");
-            this.add_class(div, "slide");
-            this.body.replaceChild(div, heading);
-            div.appendChild(heading);
-            while (node) {
-                if (node.nodeType == 1) { // an element
-                    if (node.nodeName == "H1" || node.nodeName == "h1") {
-                        break;
-                    }
-                    if (node.nodeName == "DIV" || node.nodeName == "div") {
-                        if (this.has_class(node, "slide")) {
-                            break;
-                        }
-                        if (this.has_class(node, "handout")) {
-                            break;
-                        }
-                    }
-                }
-                next = node.nextSibling;
-                node = this.body.removeChild(node);
-                div.appendChild(node);
-                node = next;
-            }
-        }
-    },
-    attach_touch_handers: function(slides) {
-        var i, slide;
-        for (i = 0; i < slides.length; ++i) {
-            slide = slides[i];
-            this.add_listener(slide, "touchstart", this.touchstart);
-            this.add_listener(slide, "touchmove", this.touchmove);
-            this.add_listener(slide, "touchend", this.touchend);
-        }
-    },
-    // return new array of all slides
-    collect_slides: function () {
-        var slides = new Array();
-        var divs = this.body.getElementsByTagName("div");
-        for (var i = 0; i < divs.length; ++i) {
-            div = divs.item(i);
-            if (this.has_class(div, "slide")) {
-                // add slide to collection
-                slides[slides.length] = div;
-                // hide each slide as it is found
-                this.add_class(div, "hidden");
-                // add dummy <br> at end for scrolling hack
-                var node1 = document.createElement("br");
-                div.appendChild(node1);
-                var node2 = document.createElement("br");
-                div.appendChild(node2);
-            } else if (this.has_class(div, "background")) {
-                // work around for Firefox SVG reload bug
-                // which otherwise replaces 1st SVG graphic with 2nd
-                div.style.display = "block";
-            }
-        }
-        this.slides = slides;
-    },
-    // return new array of all <div class="handout">
-    collect_notes: function () {
-        var notes = new Array();
-        var divs = this.body.getElementsByTagName("div");
-        for (var i = 0; i < divs.length; ++i) {
-            div = divs.item(i);
-            if (this.has_class(div, "handout")) {
-                // add note to collection
-                notes[notes.length] = div;
-                // and hide it
-                this.add_class(div, "hidden");
-            }
-        }
-        this.notes = notes;
-    },
-    // return new array of all <div class="background">
-    // including named backgrounds e.g. class="background titlepage"
-    collect_backgrounds: function () {
-        var backgrounds = new Array();
-        var divs = this.body.getElementsByTagName("div");
-        for (var i = 0; i < divs.length; ++i) {
-            div = divs.item(i);
-            if (this.has_class(div, "background")) {
-                // add background to collection
-                backgrounds[backgrounds.length] = div;
-                // and hide it
-                this.add_class(div, "hidden");
-            }
-        }
-        this.backgrounds = backgrounds;
-    },
-    // set click handlers on all anchors
-    patch_anchors: function () {
-        var self = w3c_slidy;
-        var handler = function (event) {
-            // compare this.href with location.href
-            // for link to another slide in this doc
-            if (self.page_address(this.href) == self.page_address(
-                location.href)) { // yes, so find new slide number
-                var newslidenum = self.find_slide_number(this.href);
-                if (newslidenum != self.slide_number) {
-                    var slide = self.slides[self.slide_number];
-                    self.hide_slide(slide);
-                    self.slide_number = newslidenum;
-                    slide = self.slides[self.slide_number];
-                    self.show_slide(slide);
-                    self.set_location();
-                }
-            } else {
-                w3c_slidy.stop_propagation(event);
-            }
-            this.blur();
-            self.disable_slide_click = true;
-        };
-        var anchors = this.body.getElementsByTagName("a");
-        for (var i = 0; i < anchors.length; ++i) {
-            if (window.addEventListener) {
-                anchors[i].addEventListener("click", handler, false);
-            } else {
-                anchors[i].attachEvent("onclick", handler);
-            }
-        }
-    },
-    // ### CHECK ME ### see which functions are invoked via setTimeout
-    // either directly or indirectly for use of w3c_slidy vs this
-    show_slide_number: function () {
-        var timer = w3c_slidy.get_timer();
-        w3c_slidy.slide_number_element.innerHTML = timer +
-            w3c_slidy.localize("slide") + " " +
-            (w3c_slidy.slide_number + 1) + "/" + w3c_slidy.slides.length;
-    },
-    // every 200mS check if the location has been changed as a
-    // result of the user activating the Back button/menu item
-    // doesn't work for Opera < 9.5
-    check_location: function () {
-        var hash = location.hash;
-        if (w3c_slidy.slide_number > 0 && (hash == "" || hash == "#")) {
-            w3c_slidy.goto_slide(0);
-        } else if (hash.length > 2 &&
-            hash != "#(" + (w3c_slidy.slide_number + 1)+")") {
-            var num = parseInt(location.hash.substr(2));
-            if (!isNaN(num)) {
-                w3c_slidy.goto_slide(num - 1);
-            }
-        }
-        if (w3c_slidy.time_left && w3c_slidy.slide_number > 0) {
-            w3c_slidy.show_slide_number();
-            if (w3c_slidy.time_left > 0) {
-                w3c_slidy.time_left -= 200;
-            }
-        }
-    },
-    get_timer: function () {
-        var timer = "";
-        if (w3c_slidy.time_left) {
-            var mins, secs;
-            secs = Math.floor(w3c_slidy.time_left/1000);
-            mins = Math.floor(secs / 60);
-            secs = secs % 60;
-            timer = (mins ? mins+"m" : "") + secs + "s ";
-        }
-        return timer;
-    },
-    // this doesn't push location onto history stack for IE
-    // for which a hidden iframe hack is needed: load page into
-    // the iframe with script that set's parent's location.hash
-    // but that won't work for standalone use unless we can
-    // create the page dynamically via a javascript: URL
-    // ### use history.pushState if available
-    set_location: function () {
-        var uri = w3c_slidy.page_address(location.href);
-        var hash = "#(" + (w3c_slidy.slide_number+1) + ")";
-        if (w3c_slidy.slide_number >= 0)
-        uri = uri + hash;
-        if (typeof(history.pushState) != "undefined") {
-            document.title = w3c_slidy.title + " (" +
-                (w3c_slidy.slide_number+1) + ")";
-            history.pushState(0, document.title, hash);
-            w3c_slidy.show_slide_number();
-            w3c_slidy.notify_observers();
-            return;
-        }
-        if (uri != location.href) {// && !khtml
-            location.href = uri;
-        }
-        if (this.khtml) {
-            hash = "(" + (w3c_slidy.slide_number+1) + ")";
-        }
-        if (!this.ie && location.hash != hash && location.hash != "") {
-            location.hash = hash;
-        }
-        document.title = w3c_slidy.title + " (" +
-            (w3c_slidy.slide_number+1) + ")";
-        w3c_slidy.show_slide_number();
-        w3c_slidy.notify_observers();
-    },
-    notify_observers: function () {
-        var slide = this.slides[this.slide_number];
-        for (var i = 0; i < this.observers.length; ++i) {
-            this.observers[i](this.slide_number+1,
-                this.find_heading(slide).innerText, location.href);
-        }
-    },
-    add_observer: function (observer) {
-        for (var i = 0; i < this.observers.length; ++i) {
-            if (observer == this.observers[i]) {
-                return;
-            }
-        }
-        this.observers.push(observer);
-    },
-    remove_observer: function (o)
-    {
-        for (var i = 0; i < this.observers.length; ++i) {
-            if (observer == this.observers[i]) {
-                this.observers.splice(i,1);
-                break;
-            }
-        }
-    },
-    page_address: function (uri) {
-        var i = uri.indexOf("#");
-        if (i < 0) {
-            i = uri.indexOf("%23");
-        }
-        // check if anchor is entire page
-        if (i < 0) {
-            return uri;  // yes
-        }
-        return uri.substr(0, i);
-    },
-    // history hack with thanks to Bertrand Le Roy
-    push_hash: function (hash) {
-        if (hash == "") {
-            hash = "#(1)";
-        }
-        window.location.hash = hash;
-        var doc = document.getElementById(
-            "historyFrame").contentWindow.document;
-        doc.open("javascript:'<html></html>'");
-        doc.write("<html><head><script>" +
-            "window.parent.w3c_slidy.on_frame_loaded('"+
-            (hash) + "');</script></head><body>hello mum</body></html>");
-        doc.close();
-    },
-    // find current slide based upon location
-    // first find target anchor and then look
-    // for associated div element enclosing it
-    // finally map that to slide number
-    find_slide_number: function (uri) {
-        // first get anchor from page location
-        var i = uri.indexOf("#");
-        // check if anchor is entire page
-        if (i < 0) {
-            return 0;  // yes
-        }
-        var anchor = unescape(uri.substr(i+1));
-        // now use anchor as XML ID to find target
-        var target = document.getElementById(anchor);
-        if (!target) {
-            // does anchor look like "(2)" for slide 2 ??
-            // where first slide is (1)
-            var re = /\((\d)+\)/;
-            if (anchor.match(re)) {
-                var num = parseInt(anchor.substring(1, anchor.length-1));
-                if (num > this.slides.length) {
-                  num = 1;
-                }
-                if (--num < 0) {
-                    num = 0;
-                }
-                return num;
-            }
-            // accept [2] for backwards compatibility
-            re = /\[(\d)+\]/;
-            if (anchor.match(re)) {
-                var num = parseInt(anchor.substring(1, anchor.length-1));
-                if (num > this.slides.length) {
-                    num = 1;
-                }
-                if (--num < 0) {
-                    num = 0;
-                }
-                return num;
-            }
-            // oh dear unknown anchor
-            return 0;
-        }
-        // search for enclosing slide
-        while (true) {
-            // browser coerces html elements to uppercase!
-            if (target.nodeName.toLowerCase() == "div" &&
-                this.has_class(target, "slide")) {
-                // found the slide element
-                break;
-            }
-            // otherwise try parent element if any
-            target = target.parentNode;
-            if (!target) {
-                return 0;   // no luck!
-            }
-        }
-        for (i = 0; i < slides.length; ++i) {
-          if (slides[i] == target)
-            return i;  // success
-        }
-        // oh dear still no luck
-        return 0;
-    },
-    previous_slide: function (incremental) {
-        if (!w3c_slidy.view_all) {
-            var slide;
-            if ((incremental || w3c_slidy.slide_number == 0) &&
-                w3c_slidy.last_shown != null) {
-                w3c_slidy.last_shown =
-                    w3c_slidy.hide_previous_item(w3c_slidy.last_shown);
-                w3c_slidy.set_eos_status(false);
-            } else if (w3c_slidy.slide_number > 0) {
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.hide_slide(slide);
-
-                w3c_slidy.slide_number = w3c_slidy.slide_number - 1;
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.set_visibility_all_incremental("visible");
-                w3c_slidy.last_shown =
-                    w3c_slidy.previous_incremental_item(null);
-                w3c_slidy.set_eos_status(true);
-                w3c_slidy.show_slide(slide);
-            }
-            w3c_slidy.set_location();
-            if (!w3c_slidy.ns_pos) {
-                w3c_slidy.refresh_toolbar(200);
-            }
-        }
-    },
-    next_slide: function (incremental) {
-        if (!w3c_slidy.view_all) {
-            var slide, last = w3c_slidy.last_shown;
-            if (incremental ||
-                w3c_slidy.slide_number == w3c_slidy.slides.length - 1) {
-                w3c_slidy.last_shown =
-                    w3c_slidy.reveal_next_item(w3c_slidy.last_shown);
-            }
-            if ((!incremental || w3c_slidy.last_shown == null) &&
-                 w3c_slidy.slide_number < w3c_slidy.slides.length - 1) {
-                 slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                 w3c_slidy.hide_slide(slide);
-                 w3c_slidy.slide_number = w3c_slidy.slide_number + 1;
-                 slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                 w3c_slidy.last_shown = null;
-                 w3c_slidy.set_visibility_all_incremental("hidden");
-                 w3c_slidy.show_slide(slide);
-            } else if (!w3c_slidy.last_shown) {
-                if (last && incremental) {
-                    w3c_slidy.last_shown = last;
-                }
-            }
-            w3c_slidy.set_location();
-            w3c_slidy.set_eos_status(!w3c_slidy.next_incremental_item(
-                w3c_slidy.last_shown));
-            if (!w3c_slidy.ns_pos) {
-                w3c_slidy.refresh_toolbar(200);
-            }
-         }
-    },
-    // to first slide with nothing revealed
-    // i.e. state at start of presentation
-    first_slide: function () {
-        if (!w3c_slidy.view_all) {
-            var slide;
-            if (w3c_slidy.slide_number != 0) {
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.hide_slide(slide);
-                w3c_slidy.slide_number = 0;
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.last_shown = null;
-                w3c_slidy.set_visibility_all_incremental("hidden");
-                w3c_slidy.show_slide(slide);
-            }
-            w3c_slidy.set_eos_status(
-                !w3c_slidy.next_incremental_item(w3c_slidy.last_shown));
-            w3c_slidy.set_location();
-        }
-    },
-    // goto last slide with everything revealed
-    // i.e. state at end of presentation
-    last_slide: function () {
-        if (!w3c_slidy.view_all) {
-            var slide;
-            w3c_slidy.last_shown = null;
-            if (w3c_slidy.last_shown == null &&
-              w3c_slidy.slide_number < w3c_slidy.slides.length - 1) {
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.hide_slide(slide);
-                w3c_slidy.slide_number = w3c_slidy.slides.length - 1;
-                slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.set_visibility_all_incremental("visible");
-                w3c_slidy.last_shown = w3c_slidy.previous_incremental_item(null);
-                w3c_slidy.show_slide(slide);
-            } else {
-                w3c_slidy.set_visibility_all_incremental("visible");
-                w3c_slidy.last_shown =
-                    w3c_slidy.previous_incremental_item(null);
-            }
-            w3c_slidy.set_eos_status(true);
-            w3c_slidy.set_location();
-        }
-    },
-    // ### check this and consider add/remove class
-    set_eos_status: function (state) {
-        if (this.eos) {
-            this.eos.style.color = (state ? "rgb(240,240,240)" : "red");
-        }
-    },
-    // first slide is 0
-    goto_slide: function (num) {
-        var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        w3c_slidy.hide_slide(slide);
-        w3c_slidy.slide_number = num;
-        slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        w3c_slidy.last_shown = null;
-        w3c_slidy.set_visibility_all_incremental("hidden");
-        w3c_slidy.set_eos_status(!w3c_slidy.next_incremental_item(
-            w3c_slidy.last_shown));
-        document.title = w3c_slidy.title + " (" +
-            (w3c_slidy.slide_number+1) + ")";
-        w3c_slidy.show_slide(slide);
-        w3c_slidy.show_slide_number();
-    },
-    show_slide: function (slide) {
-        this.sync_background(slide);
-        this.remove_class(slide, "hidden");
-    },
-    hide_slide: function (slide) {
-        this.add_class(slide, "hidden");
-    },
-    set_focus: function (element) {
-        if (element) {
-            element.focus();
-        } else {
-            w3c_slidy.help_anchor.focus();
-            setTimeout(function() {
-                w3c_slidy.help_anchor.blur();
-            }, 1);
-        }
-    },
-    // show just the backgrounds pertinent to this slide
-    // when slide background-color is transparent
-    // this should now work with rgba color values
-    sync_background: function (slide) {
-        var background;
-        var bgColor;
-        if (slide.currentStyle) {
-          bgColor = slide.currentStyle["backgroundColor"];
-        } else if (document.defaultView) {
-            var styles = document.defaultView.getComputedStyle(slide,null);
-            if (styles) {
-                bgColor = styles.getPropertyValue("background-color");
-            } else { // broken implementation probably due Safari or Konqueror
-                bgColor = "transparent";
-            }
-        } else {
-            bgColor == "transparent";
-        }
-        if (bgColor == "transparent" ||
-            bgColor.indexOf("rgba") >= 0 ||
-            bgColor.indexOf("opacity") >= 0) {
-            var slideClass = this.get_class_list(slide);
-            for (var i = 0; i < this.backgrounds.length; i++) {
-                background = this.backgrounds[i];
-                var bgClass = this.get_class_list(background);
-                if (this.matching_background(slideClass, bgClass)) {
-                    this.remove_class(background, "hidden");
-                } else {
-                    this.add_class(background, "hidden");
-                }
-            }
-        } else {// forcibly hide all backgrounds
-            this.hide_backgrounds();
-        }
-    },
-    hide_backgrounds: function () {
-        for (var i = 0; i < this.backgrounds.length; i++) {
-            background = this.backgrounds[i];
-            this.add_class(background, "hidden");
-        }
-    },
-    // compare classes for slide and background
-    matching_background: function (slideClass, bgClass) {
-        var i, count, pattern, result;
-        // define pattern as regular expression
-        pattern = /\w+/g;
-        // check background class names
-        result = bgClass.match(pattern);
-        for (i = count = 0; i < result.length; i++) {
-            if (result[i] == "hidden") {
-                continue;
-            }
-            if (result[i] == "background") {
-                continue;
-            }
-            ++count;
-        }
-        if (count == 0) { // default match
-            return true;
-        }
-        // check for matches and place result in array
-        result = slideClass.match(pattern);
-        // now check if desired name is present for background
-        for (i = count = 0; i < result.length; i++) {
-            if (result[i] == "hidden") {
-                continue;
-            }
-            if (this.has_token(bgClass, result[i])) {
-                return true;
-            }
-        }
-        return false;
-    },
-    resized: function () {
-        var width = 0;
-        width = window.innerWidth;  // Non IE browser
-        var height = 0;
-        height = window.innerHeight;  // Non IE browser
-        if (height && (width/height > 1.05*1024/768)) {
-            width = height * 1024.0/768;
-        }
-        // IE fires onresize even when only font size is changed!
-        // so we do a check to avoid blocking < and > actions
-        if (width != w3c_slidy.last_width || height != w3c_slidy.last_height) {
-            if (width >= 1100) {
-                w3c_slidy.size_index = 5;    // 4
-            } else if (width >= 1000) {
-                w3c_slidy.size_index = 4;    // 3
-            } else if (width >= 800) {
-                w3c_slidy.size_index = 3;    // 2
-            } else if (width >= 600) {
-                w3c_slidy.size_index = 2;    // 1
-            } else if (width) {
-                w3c_slidy.size_index = 0;
-            }
-            // add in font size adjustment from meta element e.g.
-            // <meta name="font-size-adjustment" content="-2" >
-            // useful when slides have too much content ;-)
-            if (0 <= w3c_slidy.size_index + w3c_slidy.size_adjustment &&
-                w3c_slidy.size_index + w3c_slidy.size_adjustment <
-                w3c_slidy.sizes.length) {
-                w3c_slidy.size_index = w3c_slidy.size_index +
-                    w3c_slidy.size_adjustment;
-            }
-            // enables cross browser use of relative width/height
-            // on object elements for use with SVG and Flash media
-            w3c_slidy.adjust_object_dimensions(width, height);
-            if (this.body.style.fontSize !=
-                w3c_slidy.sizes[w3c_slidy.size_index]) {
-                this.body.style.fontSize =
-                    w3c_slidy.sizes[w3c_slidy.size_index];
-            }
-            w3c_slidy.last_width = width;
-            w3c_slidy.last_height = height;
-            // force reflow to work around Mozilla bug
-            if (w3c_slidy.ns_pos) {
-                var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-                w3c_slidy.hide_slide(slide);
-                w3c_slidy.show_slide(slide);
-            }
-            // force correct positioning of toolbar
-            w3c_slidy.refresh_toolbar(200);
-        }
-    },
-    scrolled: function () {
-        if (w3c_slidy.toolbar && !w3c_slidy.ns_pos) {
-            w3c_slidy.hack_offset = w3c_slidy.scroll_x_offset();
-            // hide toolbar
-            w3c_slidy.toolbar.style.display = "none";
-            // make it reappear later
-            if (w3c_slidy.scrollhack == 0 && !w3c_slidy.view_all) {
-                setTimeout(function () {w3c_slidy.show_toolbar(); }, 1000);
-                w3c_slidy.scrollhack = 1;
-            }
-        }
-    },
-        hide_toolbar: function () {
-        w3c_slidy.add_class(w3c_slidy.toolbar, "hidden");
-        window.focus();
-    },
-    refresh_toolbar: function (interval) {
-        if (!w3c_slidy.ns_pos) {
-            w3c_slidy.hide_toolbar();
-            setTimeout(function () {
-                w3c_slidy.show_toolbar();
-            }, interval);
-        }
-    },
-    // restores toolbar after short delay
-    show_toolbar: function () {
-        if (w3c_slidy.want_toolbar) {
-            w3c_slidy.toolbar.style.display = "block";
-
-            if (!w3c_slidy.ns_pos) {
-                // adjust position to allow for scrolling
-                var xoffset = w3c_slidy.scroll_x_offset();
-                w3c_slidy.toolbar.style.left = xoffset;
-                w3c_slidy.toolbar.style.right = xoffset;
-                w3c_slidy.toolbar.style.bottom = 0; //bottom;
-            }
-            w3c_slidy.remove_class(w3c_slidy.toolbar, "hidden");
-        }
-        w3c_slidy.scrollhack = 0;
-        // set the keyboard focus to the help link on the
-        // toolbar to ensure that document has the focus
-        // IE doesn't always work with window.focus()
-        // and this hack has benefit of Enter for help
-        try {
-          if (!w3c_slidy.opera) {
-                w3c_slidy.set_focus();
-          }
-        } catch (e) {
-        }
-    },
-    // invoked via F key
-    toggle_toolbar: function () {
-        if (!w3c_slidy.view_all) {
-            if (w3c_slidy.has_class(w3c_slidy.toolbar, "hidden")) {
-                w3c_slidy.remove_class(w3c_slidy.toolbar, "hidden")
-                w3c_slidy.want_toolbar = 1;
-            } else {
-                w3c_slidy.add_class(w3c_slidy.toolbar, "hidden")
-                w3c_slidy.want_toolbar = 0;
-            }
-        }
-    },
-    scroll_x_offset: function () {
-        if (window.pageXOffset) {
-            return self.pageXOffset;
-        }
-        if (document.documentElement &&
-            document.documentElement.scrollLeft) {
-            return document.documentElement.scrollLeft;
-        }
-        if (this.body) {
-            return this.body.scrollLeft;
-        }
-        return 0;
-    },
-    scroll_y_offset: function () {
-        if (window.pageYOffset) {
-            return self.pageYOffset;
-        }
-        if (document.documentElement &&
-            document.documentElement.scrollTop) {
-            return document.documentElement.scrollTop;
-        }
-        if (this.body) {
-            return this.body.scrollTop;
-        }
-        return 0;
-    },
-    // looking for a way to determine height of slide content
-    // the slide itself is set to the height of the window
-    optimize_font_size: function () {
-        var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        //var dh = documentHeight(); //getDocHeight(document);
-        var dh = slide.scrollHeight;
-        var wh = getWindowHeight();
-        var u = 100 * dh / wh;
-        alert("window utilization = " + u + "% (doc "
-            + dh + " win " + wh + ")");
-    },
-    // from document object
-    get_doc_height: function (doc) {
-        if (!doc) {
-            doc = document;
-        }
-        if (doc && doc.body && doc.body.offsetHeight) {
-            return doc.body.offsetHeight;  // ns/gecko syntax
-        }
-        if (doc && doc.body && doc.body.scrollHeight) {
-            return doc.body.scrollHeight;
-        }
-        alert("couldn't determine document height");
-    },
-    get_window_height: function () {
-        return window.innerHeight;
-    },
-    document_height: function () {
-        var sh, oh;
-        sh = this.body.scrollHeight;
-        oh = this.body.offsetHeight;
-        if (sh && oh) {
-            return (sh > oh ? sh : oh);
-        }
-        // no idea!
-        return 0;
-    },
-    smaller: function () {
-        if (w3c_slidy.size_index > 0) {
-            --w3c_slidy.size_index;
-        }
-        w3c_slidy.toolbar.style.display = "none";
-        this.body.style.fontSize = w3c_slidy.sizes[w3c_slidy.size_index];
-        var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        w3c_slidy.hide_slide(slide);
-        w3c_slidy.show_slide(slide);
-        setTimeout(function () {
-            w3c_slidy.show_toolbar();
-        }, 50);
-    },
-    bigger: function () {
-        if (w3c_slidy.size_index < w3c_slidy.sizes.length - 1) {
-            ++w3c_slidy.size_index;
-        }
-        w3c_slidy.toolbar.style.display = "none";
-        this.body.style.fontSize = w3c_slidy.sizes[w3c_slidy.size_index];
-        var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        w3c_slidy.hide_slide(slide);
-        w3c_slidy.show_slide(slide);
-        setTimeout(function () {
-            w3c_slidy.show_toolbar();
-        }, 50);
-    },
-    // enables cross browser use of relative width/height
-    // on object elements for use with SVG and Flash media
-    // with thanks to Ivan Herman for the suggestion
-    adjust_object_dimensions: function (width, height) {
-        for ( var i = 0; i < w3c_slidy.objects.length; i++ )
-        {
-            var obj = this.objects[i];
-            var mimeType = obj.getAttribute("type");
-            if (mimeType == "image/svg+xml" ||
-                mimeType == "application/x-shockwave-flash") {
-                if ( !obj.initialWidth ) {
-                    obj.initialWidth = obj.getAttribute("width");
-                }
-                if ( !obj.initialHeight ) {
-                    obj.initialHeight = obj.getAttribute("height");
-                }
-                if ( obj.initialWidth && obj.initialWidth.charAt(
-                    obj.initialWidth.length-1) == "%" ) {
-                    var w = parseInt(obj.initialWidth.slice(0, obj.initialWidth.length-1));
-                    var newW = width * (w/100.0);
-                    obj.setAttribute("width",newW);
-                }
-                if ( obj.initialHeight && obj.initialHeight.charAt(
-                    obj.initialHeight.length - 1) == "%" ) {
-                    var h = parseInt(obj.initialHeight.slice(0,
-                        obj.initialHeight.length-1));
-                    var newH = height * (h/100.0);
-                    obj.setAttribute("height", newH);
-                }
-            }
-        }
-    },
-    // needed for Opera to inhibit default behavior
-    // since Opera delivers keyPress even if keyDown
-    // was cancelled
-    key_press: function (event) {
-        if (!event) {
-            event = window.event;
-        }
-        if (!w3c_slidy.key_wanted) {
-            return w3c_slidy.cancel(event);
-        }
-        return true;
-    },
-    //  See e.g. http://www.quirksmode.org/js/events/keys.html for keycodes
-    key_down: function (event) {
-        var key, target, tag;
-        w3c_slidy.key_wanted = true;
-        if (!event) {
-            event = window.event;
-        }
-        // kludge around NS/IE differences
-        if (window.event) {
-          key = window.event.keyCode;
-          target = window.event.srcElement;
-        } else if (event.which) {
-            key = event.which;
-            target = event.target;
-        } else {
-            return true; // Yikes! unknown browser
-        }
-        // ignore event if key value is zero
-        // as for alt on Opera and Konqueror
-        if (!key) {
-           return true;
-        }
-        // avoid interfering with keystroke
-        // behavior for non-slidy chrome elements
-        if (!w3c_slidy.slidy_chrome(target) &&
-            w3c_slidy.special_element(target)) {
-            return true;
-        }
-        // check for concurrent control/command/alt key
-        // but are these only present on mouse events?
-        if (event.ctrlKey || event.altKey || event.metaKey) {
-            return true;
-        }
-        // dismiss table of contents if visible
-        if (w3c_slidy.is_shown_toc() && key != 9 && key != 16 &&
-            key != 38 && key != 40) {
-            w3c_slidy.hide_table_of_contents(true);
-            if (key == 27 || key == 84 || key == 67) {
-                return w3c_slidy.cancel(event);
-            }
-        }
-        if (key == 34) {// Page Down
-            if (w3c_slidy.view_all) {
-                return true;
-            }
-            w3c_slidy.next_slide(false);
-            return w3c_slidy.cancel(event);
-        } else if (key == 33) { // Page Up
-            if (w3c_slidy.view_all) {
-                return true;
-            }
-            w3c_slidy.previous_slide(false);
-            return w3c_slidy.cancel(event);
-        } else if (key == 32) { // space bar
-            w3c_slidy.next_slide(true);
-            return w3c_slidy.cancel(event);
-        } else if (key == 37) { // Left arrow
-            w3c_slidy.previous_slide(!event.shiftKey);
-            return w3c_slidy.cancel(event);
-        } else if (key == 36) { // Home
-            w3c_slidy.first_slide();
-            return w3c_slidy.cancel(event);
-        } else if (key == 35) { // End
-            w3c_slidy.last_slide();
-            return w3c_slidy.cancel(event);
-        } else if (key == 39) { // Right arrow
-            w3c_slidy.next_slide(!event.shiftKey);
-            return w3c_slidy.cancel(event);
-        } else if (key == 13) {// Enter
-            if (w3c_slidy.outline) {
-                if (w3c_slidy.outline.visible) {
-                    w3c_slidy.fold(w3c_slidy.outline);
-                } else {
-                    w3c_slidy.unfold(w3c_slidy.outline);
-                }
-                return w3c_slidy.cancel(event);
-            }
-        } else if (key == 188) { // < for smaller fonts
-            w3c_slidy.smaller();
-            return w3c_slidy.cancel(event);
-        } else if (key == 190) { // > for larger fonts
-            w3c_slidy.bigger();
-            return w3c_slidy.cancel(event);
-        } else if (key == 189 || key == 109) { // - for smaller fonts
-            w3c_slidy.smaller();
-            return w3c_slidy.cancel(event);
-        } else if (key == 187 || key == 191 || key == 107) {
-            // = +  for larger fonts
-            w3c_slidy.bigger();
-            return w3c_slidy.cancel(event);
-        } else if (key == 83) { // S for smaller fonts
-            w3c_slidy.smaller();
-            return w3c_slidy.cancel(event);
-        } else if (key == 66) { // B for larger fonts
-            w3c_slidy.bigger();
-            return w3c_slidy.cancel(event);
-        } else if (key == 90) { // Z for last slide
-            w3c_slidy.last_slide();
-            return w3c_slidy.cancel(event);
-        } else if (key == 70) { // F for toggle toolbar
-            w3c_slidy.toggle_toolbar();
-            return w3c_slidy.cancel(event);
-        } else if (key == 65) { // A for toggle view single/all slides
-            w3c_slidy.toggle_view();
-            return w3c_slidy.cancel(event);
-        } else if (key == 75) { // toggle action of left click for next page
-            w3c_slidy.mouse_click_enabled = !w3c_slidy.mouse_click_enabled;
-            var alert_msg = (w3c_slidy.mouse_click_enabled ?
-                "enabled" : "disabled") +  " mouse click advance";
-            alert(w3c_slidy.localize(alert_msg));
-            return w3c_slidy.cancel(event);
-        } else if (key == 84 || key == 67) { // T or C for table of contents
-            if (w3c_slidy.toc) {
-                w3c_slidy.toggle_table_of_contents();
-            }
-            return w3c_slidy.cancel(event);
-        } else if (key == 72) {// H for help
-            window.location = w3c_slidy.help_page;
-            return w3c_slidy.cancel(event);
-        } else if (key == 85 && w3c_slidy.up_link) { //U for up link
-            window.location = w3c_slidy.up_link;
-            return w3c_slidy.cancel(event);
-        }
-        return true;
-    },
-    // safe for both text/html and application/xhtml+xml
-    create_element: function (name) {
-        if (this.xhtml && (typeof document.createElementNS != 'undefined')) {
-          return document.createElementNS("http://www.w3.org/1999/xhtml", name);
-        }
-        return document.createElement(name);
-    },
-    get_element_style: function (elem, IEStyleProp, CSSStyleProp) {
-        if (elem.currentStyle) {
-            return elem.currentStyle[IEStyleProp];
-        } else if (window.getComputedStyle) {
-            var compStyle = window.getComputedStyle(elem, "");
-            return compStyle.getPropertyValue(CSSStyleProp);
-        }
-        return "";
-    },
-    // the string str is a whitespace separated list of tokens
-    // test if str contains a particular token, e.g. "slide"
-    has_token: function (str, token) {
-        if (str) {
-            // define pattern as regular expression
-            var pattern = /\w+/g;
-            // check for matches
-            // place result in array
-            var result = str.match(pattern);
-            // now check if desired token is present
-            for (var i = 0; i < result.length; i++) {
-                if (result[i] == token) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    },
-    get_class_list: function (element) {
-        if (typeof element.className != 'undefined') {
-            return element.className;
-        }
-        return element.getAttribute("class");
-    },
-    has_class: function (element, name) {
-        if (element.nodeType != 1) {
-            return false;
-        }
-        var regexp = new RegExp("(^| )" + name + "\W*");
-        if (typeof element.className != 'undefined') {
-            return regexp.test(element.className);
-        }
-        return regexp.test(element.getAttribute("class"));
-    },
-    remove_class: function (element, name) {
-        var regexp = new RegExp("(^| )" + name + "\W*");
-        var clsval = "";
-        if (typeof element.className != 'undefined') {
-            clsval = element.className;
-            if (clsval) {
-                clsval = clsval.replace(regexp, "");
-                element.className = clsval;
-            }
-        } else {
-            clsval = element.getAttribute("class");
-            if (clsval) {
-                clsval = clsval.replace(regexp, "");
-                element.setAttribute("class", clsval);
-            }
-        }
-    },
-    add_class: function (element, name) {
-        if (!this.has_class(element, name)) {
-            if (typeof element.className != 'undefined') {
-                element.className += " " + name;
-            } else {
-                var clsval = element.getAttribute("class");
-                clsval = clsval ? clsval + " " + name : name;
-                element.setAttribute("class", clsval);
-            }
-        }
-    },
-    // HTML elements that can be used with class="incremental"
-    // note that you can also put the class on containers like
-    // up, ol, dl, and div to make their contents appear
-    // incrementally. Upper case is used since this is what
-    // browsers report for HTML node names (text/html).
-    incremental_elements: null,
-    okay_for_incremental: function (name) {
-        if (!this.incremental_elements) {
-            var inclist = new Array();
-            inclist["p"] = true;
-            inclist["pre"] = true;
-            inclist["li"] = true;
-            inclist["blockquote"] = true;
-            inclist["dt"] = true;
-            inclist["dd"] = true;
-            inclist["h2"] = true;
-            inclist["h3"] = true;
-            inclist["h4"] = true;
-            inclist["h5"] = true;
-            inclist["h6"] = true;
-            inclist["span"] = true;
-            inclist["address"] = true;
-            inclist["table"] = true;
-            inclist["tr"] = true;
-            inclist["th"] = true;
-            inclist["td"] = true;
-            inclist["img"] = true;
-            inclist["object"] = true;
-            this.incremental_elements = inclist;
-        }
-        return this.incremental_elements[name.toLowerCase()];
-    },
-    next_incremental_item: function (node) {
-        var br = this.is_xhtml ? "br" : "BR";
-        var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        for (;;) {
-            node = w3c_slidy.next_node(slide, node);
-            if (node == null || node.parentNode == null) {
-                break;
-            }
-            if (node.nodeType == 1) { // ELEMENT
-                if (node.nodeName == br) {
-                    continue;
-                }
-                if (w3c_slidy.has_class(node, "incremental")
-                     && w3c_slidy.okay_for_incremental(node.nodeName)) {
-                    return node;
-                }
-                if (w3c_slidy.has_class(node.parentNode, "incremental")
-                     && !w3c_slidy.has_class(node, "non-incremental")) {
-                    return node;
-                }
-            }
-        }
-        return node;
-    },
-    previous_incremental_item: function (node) {
-        var br = this.is_xhtml ? "br" : "BR";
-        var slide = w3c_slidy.slides[w3c_slidy.slide_number];
-        for (;;) {
-            node = w3c_slidy.previous_node(slide, node);
-            if (node == null || node.parentNode == null) {
-                break;
-            }
-            if (node.nodeType == 1) {
-                if (node.nodeName == br) {
-                    continue;
-                }
-                if (w3c_slidy.has_class(node, "incremental")
-                     && w3c_slidy.okay_for_incremental(node.nodeName)) {
-                    return node;
-                }
-                if (w3c_slidy.has_class(node.parentNode, "incremental")
-                     && !w3c_slidy.has_class(node, "non-incremental")) {
-                    return node;
-                }
-            }
-        }
-        return node;
-    },
-    // set visibility for all elements on current slide with
-    // a parent element with attribute class="incremental"
-    set_visibility_all_incremental: function (value) {
-        var node = this.next_incremental_item(null);
-        if (value == "hidden") {
-            while (node) {
-                w3c_slidy.add_class(node, "invisible");
-                node = w3c_slidy.next_incremental_item(node);
-            }
-        } else {// value == "visible"
-            while (node) {
-                w3c_slidy.remove_class(node, "invisible");
-                node = w3c_slidy.next_incremental_item(node);
-            }
-        }
-    },
-    // reveal the next hidden item on the slide
-    // node is null or the node that was last revealed
-    reveal_next_item: function (node) {
-        node = w3c_slidy.next_incremental_item(node);
-        if (node && node.nodeType == 1) { // an element
-            w3c_slidy.remove_class(node, "invisible");
-        }
-        return node;
-    },
-    // exact inverse of revealNextItem(node)
-    hide_previous_item: function (node) {
-        if (node && node.nodeType == 1) { // an element
-            w3c_slidy.add_class(node, "invisible");
-        }
-        return this.previous_incremental_item(node);
-    },
-    // left to right traversal of root's content
-    next_node: function (root, node) {
-        if (node == null) {
-            return root.firstChild;
-        }
-        if (node.firstChild) {
-            return node.firstChild;
-        }
-        if (node.nextSibling) {
-            return node.nextSibling;
-        }
-        for (;;) {
-            node = node.parentNode;
-            if (!node || node == root) {
-                break;
-            }
-            if (node && node.nextSibling) {
-                return node.nextSibling;
-            }
-        }
-        return null;
-    },
-    // right to left traversal of root's content
-    previous_node: function (root, node) {
-        if (node == null) {
-          node = root.lastChild;
-          if (node) {
-            while (node.lastChild) {
-                node = node.lastChild;
-            }
-          }
-          return node;
-        }
-        if (node.previousSibling) {
-            node = node.previousSibling;
-            while (node.lastChild)
-            node = node.lastChild;
-            return node;
-        }
-        if (node.parentNode != root) {
-            return node.parentNode;
-        }
-        return null;
-    },
-    previous_sibling_element: function (el) {
-        el = el.previousSibling;
-        while (el && el.nodeType != 1) {
-          el = el.previousSibling;
-        }
-        return el;
-    },
-    next_sibling_element: function (el) {
-        el = el.nextSibling;
-        while (el && el.nodeType != 1) {
-            el = el.nextSibling;
-        }
-        return el;
-    },
-    first_child_element: function (el) {
-        var node;
-        for (node = el.firstChild; node; node = node.nextSibling) {
-            if (node.nodeType == 1) {
-                break;
-            }
-        }
-        return node;
-    },
-    first_tag: function (element, tag) {
-        var node;
-        if (!this.is_xhtml) {
-          tag = tag.toUpperCase();
-        }
-        for (node = element.firstChild; node; node = node.nextSibling) {
-            if (node.nodeType == 1 && node.nodeName == tag) {
-                break;
-            }
-        }
-        return node;
-    },
-    hide_selection: function () {
-        if (window.getSelection) {// Firefox, Chromium, Safari, Opera
-            var selection = window.getSelection();
-            if (selection.rangeCount > 0) {
-                var range = selection.getRangeAt(0);
-                range.collapse (false);
-            }
-        } else {// Internet Explorer
-            var textRange = document.selection.createRange ();
-            textRange.collapse (false);
-        }
-    },
-    get_selected_text: function () {
-        try {
-            if (window.getSelection) {
-                return window.getSelection().toString();
-            }
-            if (document.getSelection) {
-                return document.getSelection().toString();
-            }
-            if (document.selection) {
-                return document.selection.createRange().text;
-            }
-        } catch (e) {
-        }
-        return "";
-    },
-    // make note of length of selected text
-    // as this evaluates to zero in click event
-    mouse_button_up: function (e) {
-        w3c_slidy.selected_text_len = w3c_slidy.get_selected_text().length;
-    },
-    mouse_button_down: function (e) {
-        w3c_slidy.selected_text_len = w3c_slidy.get_selected_text().length;
-        w3c_slidy.mouse_x = e.clientX;
-        w3c_slidy.mouse_y = e.clientY;
-    },
-    // right mouse button click is reserved for context menus
-    // it is more reliable to detect rightclick than leftclick
-    mouse_button_click: function (e) {
-        if (!e) {
-            var e = window.event;
-        }
-        if (Math.abs(e.clientX -w3c_slidy.mouse_x) +
-            Math.abs(e.clientY -w3c_slidy.mouse_y) > 10) {
-            return true;
-        }
-        if (w3c_slidy.selected_text_len > 0) {
-            return true;
-        }
-        var rightclick = false;
-        var leftclick = false;
-        var middleclick = false;
-        var target;
-        if (!e) {
-            var e = window.event;
-        }
-        if (e.target) {
-            target = e.target;
-        } else if (e.srcElement) {
-            target = e.srcElement;
-        }
-        // work around Safari bug
-        if (target.nodeType == 3) {
-          target = target.parentNode;
-        }
-        if (e.which) { // all browsers except IE
-            leftclick = (e.which == 1);
-            middleclick = (e.which == 2);
-            rightclick = (e.which == 3);
-        } else if (e.button) {
-            // Konqueror gives 1 for left, 4 for middle
-            // IE6 gives 0 for left and not 1 as I expected
-            if (e.button == 4)
-            middleclick = true;
-            // all browsers agree on 2 for right button
-            rightclick = (e.button == 2);
-        } else {
-          leftclick = true;
-        }
-        if (w3c_slidy.selected_text_len > 0) {
-            w3c_slidy.stop_propagation(e);
-            e.cancel = true;
-            e.returnValue = false;
-            return false;
-        }
-        // dismiss table of contents
-        w3c_slidy.hide_table_of_contents(false);
-        // check if target is something that probably want's clicks
-        // e.g. a, embed, object, input, textarea, select, option
-        var tag = target.nodeName.toLowerCase();
-        if (w3c_slidy.mouse_click_enabled && leftclick &&
-            !w3c_slidy.special_element(target) &&
-            !target.onclick) {
-            w3c_slidy.next_slide(true);
-            w3c_slidy.stop_propagation(e);
-            e.cancel = true;
-            e.returnValue = false;
-            return false;
-        }
-        return true;
-    },
-    special_element: function (element) {
-        if (this.has_class(element, "non-interactive")) {
-            return false;
-        }
-        var tag = element.nodeName.toLowerCase();
-        return element.onkeydown ||
-            element.onclick ||
-            tag == "a" ||
-            tag == "embed" ||
-            tag == "object" ||
-            tag == "video" ||
-            tag == "audio" ||
-            tag == "svg" ||
-            tag == "canvas" ||
-            tag == "input" ||
-            tag == "textarea" ||
-            tag == "select" ||
-            tag == "option";
-    },
-    slidy_chrome: function (el) {
-        while (el) {
-            if (el == w3c_slidy.toc ||
-                el == w3c_slidy.toolbar ||
-                w3c_slidy.has_class(el, "outline")) {
-                return true;
-            }
-            el = el.parentNode;
-        }
-        return false;
-    },
-    get_key: function (e) {
-        var key;
-        // kludge around NS/IE differences
-        if (typeof window.event != "undefined") {
-            key = window.event.keyCode;
-        } else if (e.which) {
-            key = e.which;
-        }
-        return key;
-    },
-    get_target: function (e) {
-        var target;
-        if (!e) {
-            e = window.event;
-        }
-        if (e.target) {
-            target = e.target;
-        } else if (e.srcElement) {
-            target = e.srcElement;
-        }
-        if (target.nodeType != 1) {
-            target = target.parentNode;
-        }
-        return target;
-    },
-    // does display property provide correct defaults?
-    is_block: function (elem) {
-        var tag = elem.nodeName.toLowerCase();
-        return tag == "ol" || tag == "ul" || tag == "p" || tag == "dl" ||
-           tag == "li" || tag == "table" || tag == "pre" ||
-           tag == "h1" || tag == "h2" || tag == "h3" ||
-           tag == "h4" || tag == "h5" || tag == "h6" ||
-           tag == "blockquote" || tag == "address";
-    },
-    add_listener: function (element, event, handler) {
-        if (window.addEventListener) {
-            element.addEventListener(event, handler, false);
-        } else {
-            element.attachEvent("on" + event, handler);
-        }
-    },
-    // used to prevent event propagation from field controls
-    stop_propagation: function (event) {
-        event = event ? event : window.event;
-        event.cancelBubble = true;  // for IE
-        if (event.stopPropagation) {
-            event.stopPropagation();
-        }
-        return true;
-    },
-    cancel: function (event) {
-        if (event) {
-            event.cancel = true;
-            event.returnValue = false;
-            if (event.preventDefault) {
-                event.preventDefault();
-            }
-        }
-        w3c_slidy.key_wanted = false;
-        return false;
-    },
-    // for each language define an associative array
-    // and also the help text which is longer
-    strings_es: {
-        "slide":"pág.",
-        "help?":"Ayuda",
-        "contents?":"Índice",
-        "table of contents":"tabla de contenidos",
-        "Table of Contents":"Tabla de Contenidos",
-        "restart presentation":"Reiniciar presentación",
-        "restart?":"Inicio"
-    },
-    help_es:
-        "Utilice el ratón, barra espaciadora, teclas Izda/Dcha, " +
-        "o Re pág y Av pág. Use S y B para cambiar el tamaño de fuente.",
-    strings_ca: {
-        "slide":"pàg..",
-        "help?":"Ajuda",
-        "contents?":"Índex",
-        "table of contents":"taula de continguts",
-        "Table of Contents":"Taula de Continguts",
-        "restart presentation":"Reiniciar presentació",
-        "restart?":"Inici"
-    },
-    help_ca:
-        "Utilitzi el ratolí, barra espaiadora, tecles Esq./Dta. " +
-        "o Re pàg y Av pàg. Usi S i B per canviar grandària de font.",
-    strings_cs: {
-        "slide":"snímek",
-        "help?":"nápověda",
-        "contents?":"obsah",
-        "table of contents":"obsah prezentace",
-        "Table of Contents":"Obsah prezentace",
-        "restart presentation":"znovu spustit prezentaci",
-        "restart?":"restart"
-    },
-    help_cs:
-        "Prezentaci můžete procházet pomocí kliknutí myši, mezerníku, " +
-        "šipek vlevo a vpravo nebo kláves PageUp a PageDown. Písmo se " +
-        "dá zvětšit a zmenšit pomocí kláves B a S.",
-    strings_nl: {
-        "slide":"pagina",
-        "help?":"Help?",
-        "contents?":"Inhoud?",
-        "table of contents":"inhoudsopgave",
-        "Table of Contents":"Inhoudsopgave",
-        "restart presentation":"herstart presentatie",
-        "restart?":"Herstart?"
-    },
-    help_nl:
-        "Navigeer d.m.v. het muis, spatiebar, Links/Rechts toetsen, " +
-        "of PgUp en PgDn. Gebruik S en B om de karaktergrootte te veranderen.",
-    strings_de: {
-        "slide":"Seite",
-        "help?":"Hilfe",
-        "contents?":"Übersicht",
-        "table of contents":"Inhaltsverzeichnis",
-        "Table of Contents":"Inhaltsverzeichnis",
-        "restart presentation":"Präsentation neu starten",
-        "restart?":"Neustart"
-    },
-    help_de:
-        "Benutzen Sie die Maus, Leerschlag, die Cursortasten links/rechts " +
-        "oder Page up/Page Down zum Wechseln der Seiten und S und B für die " +
-        "Schriftgrösse.",
-    strings_pl: {
-        "slide":"slajd",
-        "help?":"pomoc?",
-        "contents?":"spis treści?",
-        "table of contents":"spis treści",
-        "Table of Contents":"Spis Treści",
-        "restart presentation":"Restartuj prezentację",
-        "restart?":"restart?"
-    },
-    help_pl:
-        "Zmieniaj slajdy klikając myszą, naciskając spację, strzałki " +
-        "lewo/prawo lub PgUp / PgDn. Użyj klawiszy S i B, aby zmienić " +
-        "rozmiar czczionki.",
-    strings_fr: {
-        "slide":"page",
-        "help?":"Aide",
-        "contents?":"Index",
-        "table of contents":"table des matières",
-        "Table of Contents":"Table des matières",
-        "restart presentation":"Recommencer l'exposé",
-        "restart?":"Début"
-    },
-    help_fr:
-        "Naviguez avec la souris, la barre d'espace, les flèches " +
-        "gauche/droite ou les touches Pg Up, Pg Dn. Utilisez " +
-        "les touches S et B pour modifier la taille de la police.",
-    strings_hu: {
-        "slide":"oldal",
-        "help?":"segítség",
-        "contents?":"tartalom",
-        "table of contents":"tartalomjegyzék",
-        "Table of Contents":"Tartalomjegyzék",
-        "restart presentation":"bemutató újraindítása",
-        "restart?":"újraindítás"
-    },
-    help_hu:
-        "Az oldalak közti lépkedéshez kattintson az egérrel, vagy " +
-        "használja a szóköz, a bal, vagy a jobb nyíl, illetve a Page Down, " +
-        "Page Up billentyűket. Az S és a B billentyűkkel változtathatja " +
-    "a szöveg méretét.",
-    strings_it: {
-        "slide":"pag.",
-        "help?":"Aiuto",
-        "contents?":"Indice",
-        "table of contents":"indice",
-        "Table of Contents":"Indice",
-        "restart presentation":"Ricominciare la presentazione",
-        "restart?":"Inizio"
-    },
-    help_it:
-        "Navigare con mouse, barra spazio, frecce sinistra/destra o " +
-        "PgUp e PgDn. Usare S e B per cambiare la dimensione dei caratteri.",
-    strings_el: {
-        "slide":"σελίδα",
-        "help?":"βοήθεια;",
-        "contents?":"περιεχόμενα;",
-        "table of contents":"πίνακας περιεχομένων",
-        "Table of Contents":"Πίνακας Περιεχομένων",
-        "restart presentation":"επανεκκίνηση παρουσίασης",
-        "restart?":"επανεκκίνηση;"
-    },
-    help_el:
-        "Πλοηγηθείτε με το κλίκ του ποντικιού, το space, τα βέλη " +
-        "αριστερά/δεξιά, ή Page Up και Page Down. Χρησιμοποιήστε τα" +
-        " πλήκτρα S και B για να αλλάξετε το μέγεθος της γραμματοσειράς.",
-    strings_ja: {
-        "slide":"スライド",
-        "help?":"ヘルプ",
-        "contents?":"目次",
-        "table of contents":"目次を表示",
-        "Table of Contents":"目次",
-        "restart presentation":"最初から再生",
-        "restart?":"最初から"
-    },
-    help_ja:
-        "マウス左クリック ・ スペース ・ 左右キー " +
-        "または Page Up ・ Page Downで操作, S ・ Bでフォントサイズ変更",
-    strings_zh: {
-        "slide":"幻灯片",
-        "help?":"帮助?",
-        "contents?":"内容?",
-        "table of contents":"目录",
-        "Table of Contents":"目录",
-        "restart presentation":"重新启动展示",
-        "restart?":"重新启动?"
-    },
-    help_zh:
-        "用鼠标点击, 空格条, 左右箭头, Pg Up 和 Pg Dn 导航. " +
-        "用 S, B 改变字体大小.",
-    strings_ru: {
-        "slide":"слайд",
-        "help?":"помощь?",
-        "contents?":"содержание?",
-        "table of contents":"оглавление",
-        "Table of Contents":"Оглавление",
-        "restart presentation":"перезапустить презентацию",
-        "restart?":"перезапуск?"
-    },
-    help_ru:
-        "Перемещайтесь кликая мышкой, используя клавишу пробел, стрелки" +
-        "влево/вправо или Pg Up и Pg Dn. Клавиши S и B меняют размер шрифта.",
-    strings_sv: {
-        "slide":"sida",
-        "help?":"hjälp",
-        "contents?":"innehåll",
-        "table of contents":"innehållsförteckning",
-        "Table of Contents":"Innehållsförteckning",
-        "restart presentation":"visa presentationen från början",
-        "restart?":"börja om"
-    },
-    help_sv:
-        "Bläddra med ett klick med vänstra musknappen, mellanslagstangenten, " +
-        "vänster- och högerpiltangenterna eller tangenterna Pg Up, Pg Dn. " +
-        "Använd tangenterna S och B för att ändra textens storlek.",
-        strings: { },
-    localize: function (src) {
-        if (src == "") {
-            return src;
-        }
-        // try full language code, e.g. en-US
-        var s, lookup = w3c_slidy.strings[w3c_slidy.lang];
-        if (lookup) {
-            s = lookup[src];
-            if (s) {
-                return s;
-            }
-        }
-        // strip country code suffix, e.g.
-        // try en if undefined for en-US
-        var lg = w3c_slidy.lang.split("-");
-        if (lg.length > 1) {
-            lookup = w3c_slidy.strings[lg[0]];
-            if (lookup) {
-                s = lookup[src];
-                if (s) {
-                    return s;
-                }
-            }
-        }
-        // otherwise string as is
-        return src;
-    },
-    init_localization: function () {
-        var i18n = w3c_slidy;
-        var help_text = w3c_slidy.help_text;
-        // each such language array is declared in the localize array
-        // this is used as in  w3c_slidy.localize("foo");
-        this.strings = {
-            "es":this.strings_es,
-            "ca":this.strings_ca,
-            "cs":this.strings_cs,
-            "nl":this.strings_nl,
-            "de":this.strings_de,
-            "pl":this.strings_pl,
-            "fr":this.strings_fr,
-            "hu":this.strings_hu,
-            "it":this.strings_it,
-            "el":this.strings_el,
-            "jp":this.strings_ja,
-            "zh":this.strings_zh,
-            "ru":this.strings_ru,
-            "sv":this.strings_sv
-        },
-        i18n.strings_es[help_text] = i18n.help_es;
-        i18n.strings_ca[help_text] = i18n.help_ca;
-        i18n.strings_cs[help_text] = i18n.help_cs;
-        i18n.strings_nl[help_text] = i18n.help_nl;
-        i18n.strings_de[help_text] = i18n.help_de;
-        i18n.strings_pl[help_text] = i18n.help_pl;
-        i18n.strings_fr[help_text] = i18n.help_fr;
-        i18n.strings_hu[help_text] = i18n.help_hu;
-        i18n.strings_it[help_text] = i18n.help_it;
-        i18n.strings_el[help_text] = i18n.help_el;
-        i18n.strings_ja[help_text] = i18n.help_ja;
-        i18n.strings_zh[help_text] = i18n.help_zh;
-        i18n.strings_ru[help_text] = i18n.help_ru;
-        i18n.strings_sv[help_text] = i18n.help_sv;
-        w3c_slidy.lang = document.body.parentNode.getAttribute("lang");
-        if (!w3c_slidy.lang) {
-            w3c_slidy.lang = document.body.parentNode.getAttribute("xml:lang");
-        }
-        if (!w3c_slidy.lang) {
-            w3c_slidy.lang = "en";
-        }
-    }
-};
-// attach event listeners for initialization
-w3c_slidy.set_up();
-// hide the slides as soon as body element is available
-// to reduce annoying screen mess before the onload event
-setTimeout(w3c_slidy.hide_slides, 50);
diff --git a/src/views/layouts/WebLayout.php b/src/views/layouts/WebLayout.php
index 445899f13..5f48b2a41 100755
--- a/src/views/layouts/WebLayout.php
+++ b/src/views/layouts/WebLayout.php
@@ -319,6 +319,9 @@ class WebLayout extends Layout
         </head><?php
         $data['MOBILE'] = (!empty($_SERVER["MOBILE"])) ? 'mobile': '';
         flush();
+        $is_presentation = !empty($data["HEAD"]['page_type']) &&
+            $data["HEAD"]['page_type'] == 'presentation' &&
+            !empty($data['MODE']) && $data['MODE'] == 'read';
         ?>
         <body class="html-<?= $data['LOCALE_DIR']
             ?> html-<?= $data['WRITING_MODE'] . ' ' . $data['MOBILE'] ?>" ><?php
@@ -327,13 +330,15 @@ class WebLayout extends Layout
         $body_view = ($view_class == 'MediadetailView') ?
                 " media-detail-body " :
                 (($view_class == 'SearchView') ? " search-body " : "");
-        ?>
-        <div id="body-container" class="body-container <?=
-            $body_view ?>">
-        <div id="message" ></div><?php
-        $this->view->renderView($data);
-        if (C\QUERY_STATISTICS && (!isset($this->presentation) ||
-            !$this->presentation)) { ?>
+        if ($is_presentation) {
+            e($data['PAGE']);
+        } else {
+            ?><div id="body-container" class="body-container <?=
+                $body_view ?>">
+            <div id="message" ></div><?php
+            $this->view->renderView($data);
+        }
+        if (C\QUERY_STATISTICS && !$is_presentation) { ?>
             <div class="query-statistics"><?php
             e("<h1>" . tl('web_layout_query_statistics')."</h1>");
             e("<div><b>".
ViewGit