# impress.js Admin & Developer Guide

This guide is for developers extending impress.js, maintainers of the project, and architects who need to understand how the framework is built, how its plugin system works, and how to deploy and self-host presentations. For authoring presentations, see the companion User Guide.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Architecture Overview](#2-architecture-overview)
3. [Repository Structure](#3-repository-structure)
4. [Core Engine Deep Dive (`src/impress.js`)](#4-core-engine-deep-dive-srcimpressjs)
5. [The Library Layer (`src/lib/`)](#5-the-library-layer-srclib)
6. [The Plugin System](#6-the-plugin-system)
7. [Plugin Reference (by type)](#7-plugin-reference-by-type)
8. [The Build Pipeline](#8-the-build-pipeline)
9. [Configuration Reference](#9-configuration-reference)
10. [Deployment & Hosting](#10-deployment--hosting)
11. [Security Analysis](#11-security-analysis)
12. [Code Quality & Conventions](#12-code-quality--conventions)
13. [Testing Strategy](#13-testing-strategy)
14. [Writing a Plugin (extension recipe)](#14-writing-a-plugin-extension-recipe)
15. [Potential Improvements & Tech Debt](#15-potential-improvements--tech-debt)
16. [Quick Reference](#16-quick-reference)

---

## 1. Executive Summary

impress.js is a **client-side presentation framework** written in dependency-free, ES5-style vanilla JavaScript. It transforms a tree of `<div class="step">` elements positioned with `data-*` attributes into a zoomable, pannable, rotatable 3D presentation, driven entirely by CSS3 transforms/transitions and a small JavaScript engine.

| Aspect | Detail |
|---|---|
| Language | JavaScript (ES5-style, IIFE-wrapped, `"use strict"`) |
| Runtime dependencies | **None** (no jQuery, no frameworks, no polyfills) |
| Distribution | Single concatenated file `js/impress.js`; CDN via jsDelivr; **not on npm** |
| Architecture | Tiny core + library layer + 22 opt-in default plugins |
| Configuration | HTML `data-*` attributes (no config file, no env vars) |
| Build | `node build.js` — concatenation + Terser minification |
| Effective version | 2.0.0 (CDN tag + Getting Started); `package.json` lags at `1.1.0` |
| License | MIT |
| Browser requirements | CSS 3D Transforms, CSS Transitions, DataSet API, ClassList API |

> [!NOTE]
> There is no server component, database, runtime API, or deployment infrastructure in impress.js itself. "Deployment" means serving static HTML/JS/CSS files. This guide therefore focuses on the **build pipeline, plugin architecture, and extension model** rather than operations.

> [!IMPORTANT]
> **Version inconsistency in the repo.** `package.json` declares `"version": "1.1.0"`, but the source header in `src/impress.js` says `version: 2.0.0`, the CDN serves a `@2.0.0` tag, and `DOCUMENTATION.md` references a `v1.2.0` default-resolution change. Treat **2.0.0** as the effective released version (matching the latest tag) and the `package.json` value as stale. Several open issues (#653 "push v1.0.0 to npm", #879 "Publish Library to NPM") reflect the unfinished release/publishing process.

---

## 2. Architecture Overview

impress.js is organized as four layers that the build concatenates into one file:

```
+-----------------------------------------------------------+
|                     js/impress.js                          |
|              (generated build artifact)                    |
|                                                            |
|  +-----------------------------------------------------+   |
|  | Layer 1: CORE  (src/impress.js)                     |   |
|  |  - window.impress() factory -> API per root         |   |
|  |  - init / goto / next / prev / swipe / tear         |   |
|  |  - config + step data parsing from data-* attrs     |   |
|  |  - the canvas/root transform engine                 |   |
|  |  - plugin + library registration hooks              |   |
|  +-----------------------------------------------------+   |
|                          ^                                  |
|  +-----------------------------------------------------+   |
|  | Layer 2: LIBRARIES (src/lib/*.js)                   |   |
|  |  gc.js (garbage collection / teardown)              |   |
|  |  util.js ($/$$/byId/triggerEvent/toNumber...)       |   |
|  |  rotation.js (quaternion math for rel plugin)       |   |
|  |  -> registered via impress.addLibraryFactory()      |   |
|  |  -> exposed to plugins as api.lib.*                 |   |
|  +-----------------------------------------------------+   |
|                          ^                                  |
|  +-----------------------------------------------------+   |
|  | Layer 3: PLUGINS (src/plugins/*/*.js) x22           |   |
|  |  pre-init | pre-stepleave | init (event) plugins    |   |
|  +-----------------------------------------------------+   |
+-----------------------------------------------------------+
            |  reads at runtime
            v
   Author's HTML: <div id="impress"> <div class="step" data-*>
            |
            v
   Browser: CSS3 transforms on root (scale) + canvas (translate/rotate)
```

**The camera model.** impress.js never moves the slides. On `init()`, it wraps all steps inside a `canvas` div inside the `root` div. To "go to" a step, it computes the **inverse** of that step's transform (negated translate/rotate, reciprocal scale) and applies it to the canvas + root, so the target step lands centered and upright. The `root` element animates **scale** (zoom); the `canvas` element animates **translate + rotate**. They use staggered transition delays so zoom-in starts with movement and delays scaling, while zoom-out scales first — making transitions feel natural.

**Two coordinate concerns split across two elements** is the single most important architectural fact to internalize before reading `goto()`.

---

## 3. Repository Structure

```
impress.js/
├── index.html              Official demo (also the canonical, commented tutorial)
├── README.md               Project overview, install, browser support
├── DOCUMENTATION.md        Reference API (data-* attrs, CSS classes, JS API)
├── GettingStarted.md       Beginner tutorial
├── npm-readme.md           README variant for npm packaging
├── build.js                Build script (concat + Terser minify + examples index)
├── package.json            Build/test tooling spec (NOT a runtime dependency)
├── karma.conf.js           Karma test runner config
├── qunit_test_runner.html  Browser-based QUnit runner
├── css/
│   ├── impress-common.css  OPTIONAL sample plugin CSS (help/toolbar/progress/...)
│   └── impress-demo.css    Styling for the official demo
├── js/
│   └── impress.js          GENERATED bundle (core + libs + all plugins)
├── src/                    AUTHORED SOURCE (source of truth)
│   ├── impress.js          Core engine
│   ├── lib/
│   │   ├── gc.js           Garbage collection / teardown
│   │   ├── util.js         DOM + numeric helpers
│   │   ├── rotation.js     Quaternion rotation math (used by rel)
│   │   └── README.md
│   └── plugins/            22 default plugins, one dir each (plugin.js + README.md)
├── examples/               Demo presentations (classic-slides, cube, 3D-rotations, ...)
├── extras/                 Git submodule mount point for impress-extras (empty by default)
└── test/                   QUnit/Syn tests for core + per-plugin tests
```

> [!WARNING]
> `js/impress.js` is a **build artifact**, regenerated by `build.js` (`cat src/impress.js src/lib/*.js src/plugins/*/*.js`). Never hand-edit it — edit the files under `src/` and rebuild. The minified `js/impress.min.js` is *not* committed to the repo (it's generated at build time).

> [!NOTE]
> Each plugin directory contains its own `README.md` — these are the authoritative per-feature docs and are the basis for the User Guide's plugin catalog.

---

## 4. Core Engine Deep Dive (`src/impress.js`)

The core is a single IIFE `( function( document, window ) { ... } )( document, window )`. Key internals:

### Feature detection

On load, the core builds a `pfx()` vendor-prefix resolver (Modernizr-style) and computes `impressSupported`:

```javascript
var impressSupported =
    ( pfx( "perspective" ) !== null ) &&   // CSS 3D transforms
    ( body.classList ) &&                  // ClassList API
    ( body.dataset );                      // DataSet API
```

If unsupported, it adds `impress-not-supported` to the body, and `impress()` returns a **dummy API** (all no-op functions) so author code calling `.init()` etc. won't throw.

### The `impress(rootId)` factory

`window.impress = function(rootId)` returns (and caches) the API object for a given root id (`"impress"` by default). Each root gets its own closure with its own `stepsData`, `activeStep`, `currentState`, `steps`, `windowScale`, and a fresh set of library instances.

### Defaults & config

```javascript
var defaults = {
    width: 1920, height: 1080,
    maxScale: 3, minScale: 0,
    perspective: 1000, transitionDuration: 1000
};
```

`buildConfig()` reads the root element's `dataset` and falls back to these defaults. `impress.getConfig()` exposes the resolved config (used by `util.toNumberAdvanced` to compute `w`/`h` relative units).

### Step initialization

`initStep(el, idx)` reads each step's `data-*` attributes into a `stepsData` record:

```javascript
step = {
    translate: { x, y, z },                 // via util.toNumberAdvanced (supports w/h units)
    rotate:    { x, y, z, order },          // data-rotate-z falls back to data-rotate
    scale:     toNumber(data.scale, 1),
    transitionDuration: toNumber(data.transitionDuration, config.transitionDuration),
    el
};
```

Steps without an `id` are auto-assigned `step-N`. `initAllSteps()` runs on every `goto()`, which is **why you can mutate step coordinates at runtime** and have them apply on the next transition.

### The transform engine (`goto`)

`goto(el, duration, reason, origEvent)` is the heart of the engine:

1. Re-reads all steps (`initAllSteps()`).
2. Resolves the target via `getStep()` (accepts index, id, or element; negative indices count from the end).
3. Runs `execPreStepLeavePlugins(event)` — pre-stepleave plugins may **redirect** (`event.detail.next`), **retime** (`transitionDuration`), or **abort** (return `false`).
4. Updates `.active` classes and the `impress-on-{id}` body class.
5. Computes the **target canvas state** as the inverse of the step transform (negated rotate/translate, `1/scale`).
6. Determines `zoomin` direction to choose the transition-delay strategy.
7. Applies CSS to `root` (perspective + scale) and `canvas` (rotate + translate) with staggered `transitionDelay`.
8. Fires `impress:stepenter` after `duration + delay` via `setTimeout` (chosen over `transitionend` for simplicity across two animated elements).

### Navigation helpers

- `prev(origEvent)` / `next(origEvent)` walk `steps` in document order, wrapping around at the ends.
- `swipe(pct)` is a semi-internal API used by the touch plugin to animate a partial swipe (interpolating between current and adjacent step states); committing the transition is the caller's responsibility. It is intentionally undocumented in `DOCUMENTATION.md`.

### Events

The core triggers four custom events on the root: `impress:init`, `impress:stepenter`, `impress:stepleave`, `impress:steprefresh`. It also wires up:
- **Step state classes** (`future`/`present`/`past`) on enter/leave.
- **Hash sync**: writes `#/step-id` on enter, listens for `hashchange` to `goto()` the hashed step, and starts on the hashed step (or step 0) at init.

### The returned API

```javascript
{ init, goto, next, prev, swipe, tear, lib }
```

Plus static members: `impress.supported`, `impress.addLibraryFactory`, `impress.addPreInitPlugin`, `impress.addPreStepLeavePlugin`, `impress.getConfig`.

---

## 5. The Library Layer (`src/lib/`)

Libraries are registered with `impress.addLibraryFactory({ name: factoryFn })` and instantiated per-root inside `initLibraries()`. They're exposed to plugins as `api.lib.{name}`. A name collision throws `impress.js ERROR: Two libraries both tried to use libname: ...`.

### `gc.js` — Garbage collection / teardown

The mechanism that makes `tear()` possible. Plugins register the DOM elements and event listeners they create so they can all be cleanly removed:

| Function | Purpose |
|---|---|
| `pushElement(el)` | Track an element for removal |
| `appendChild(parent, el)` | `appendChild` + track |
| `pushEventListener(target, type, fn)` | Track a listener |
| `addEventListener(target, type, fn)` | `addEventListener` + track |
| `pushCallback(fn)` | Register arbitrary teardown logic |
| `teardown()` | Run callbacks (LIFO), remove elements, remove listeners |

`gc` also records the DOM's "virgin" state at init (`recordStartingState`) — original step ids, the body's `impress-not-supported` class, and the viewport `<meta>` — and restores it on teardown (`resetStartingState`). This is what lets you `tear()` → modify → `init()` again without residue.

> [!TIP]
> Any plugin that injects DOM or binds events **must** route them through `api.lib.gc` so `tear()` works correctly. This is the most important convention for plugin authors.

### `util.js` — DOM & numeric helpers

| Function | Purpose |
|---|---|
| `$ / $$` | `querySelector` / `querySelectorAll` (the latter returns a real Array via `arrayify`) |
| `byId(id)` | `getElementById` |
| `getElementFromHash()` | Resolve the step from the URL hash (handles both `#id` and `#/id`) |
| `getUrlParamValue(name)` | Read a URL query parameter (used by autoplay's `?impress-autoplay=`) |
| `throttle(fn, delay)` | Throttle (used by resize) |
| `toNumber(v, fallback)` | Safe numeric parse |
| `toNumberAdvanced(v, fallback)` | Like `toNumber` but resolves `Nw`/`Nh` screen-relative units against the config |
| `triggerEvent(el, name, detail)` | Dispatch a `CustomEvent` |
| `arrayify(a)` | Array-like → Array |

### `rotation.js` — Quaternion math

A self-contained quaternion/vector library (dot/cross/triple products, quaternion multiplication, coordinate↔rotation conversion, `bestRotation` selection). Its public surface is a single `translateRelative(relative, prevRotation)` function. It exists to support the **rel** plugin's `data-rel-position="relative"` feature, which must compose rotations correctly so a slide can be positioned relative to the *rotated* frame of a previous slide.

---

## 6. The Plugin System

impress.js has **three distinct plugin types**, distinguished by *when* and *how* they hook into the core.

### Type 1: Init plugins (event-driven)

The simplest and most common. They listen for `impress:init` on the `document`, grab the API from `event.detail.api`, and then bind their own behavior using the public API and `api.lib.*`. They never touch core internals.

```javascript
document.addEventListener( "impress:init", function( event ) {
    var api = event.detail.api;
    var gc  = api.lib.gc;
    // ... bind keydown/keyup/click using api.next(), api.prev(), api.goto()
}, false );
```

Examples: navigation, navigation-ui, progress, help, blackout, fullscreen, impressConsole, mobile, resize, mouse-timeout, toolbar, form, media, touch, autoplay, bookmark.

### Type 2: Pre-init plugins (attribute filters)

Registered via `impress.addPreInitPlugin(fn, weight)`. They run **synchronously at the very start of `init()`**, before the core reads step attributes. This lets them rewrite `data-*` attributes that the core will then consume.

```javascript
impress.addPreInitPlugin( rel );   // weight defaults to 10
```

- `weight` (positive integer, default 10) controls ordering — lower runs first. Non-positive weight throws.
- The canonical example is **rel**, which converts `data-rel-*` into absolute `data-x`/`data-y`/`data-z` before the core parses them.

### Type 3: Pre-stepleave plugins (transition interceptors)

Registered via `impress.addPreStepLeavePlugin(fn, weight)`. They run at the **start of every `goto()`**, before the `impress:stepleave` event. They receive the event object and can:

- **Redirect** the transition by changing `event.detail.next`.
- **Retime** it via `event.detail.transitionDuration`.
- **Abort** it entirely by returning `false`.

Examples: **goto** (redirect to an out-link), **skip** (skip a step), **stop** (cancel forward motion), **substep** (intercept to reveal a sub-element instead of leaving).

### Plugin registration summary

| Hook | Registration | Runs | Can do |
|---|---|---|---|
| Init | `addEventListener("impress:init")` | After init | Bind behavior, add DOM/UI |
| Pre-init | `impress.addPreInitPlugin(fn, weight)` | Start of `init()` | Rewrite step attributes |
| Pre-stepleave | `impress.addPreStepLeavePlugin(fn, weight)` | Start of `goto()` | Redirect/retime/abort transitions |
| Library | `impress.addLibraryFactory({name: fn})` | On first `impress(rootId)` | Provide `api.lib.{name}` services |

### Inter-plugin communication

Plugins coordinate through custom DOM events rather than direct calls. The help plugin, for instance, exposes an `impress:help:add` event; other plugins dispatch it to register a help line:

```javascript
util.triggerEvent( document, "impress:help:add",
    { command: "Left &amp; Right", text: "Previous &amp; Next step", row: 1 } );
```

This keeps plugins decoupled — the navigation plugin doesn't need a reference to the help plugin.

---

## 7. Plugin Reference (by type)

| Plugin | Type | Trigger | Notes |
|---|---|---|---|
| navigation | init | keyboard/mouse | Core navigation; maps key codes to next/prev; click-to-step |
| navigation-ui | init | toolbar | Back/Forward buttons + slide dropdown (needs toolbar) |
| touch | init | touch events | Swipe via core `swipe()` API |
| rel | **pre-init** | `data-rel-*` | Relative positioning; uses `rotation` lib; commented out registration toggles it off |
| goto | **pre-stepleave** | `data-goto*` | Out-link redirection |
| skip | **pre-stepleave** | `class="skip"` | Skip steps in linear nav |
| stop | **pre-stepleave** | `class="stop"` | Block forward wrap |
| substep | **pre-stepleave** | `class="substep"` | Reveal sub-elements before leaving |
| autoplay | init | `data-autoplay` / `?impress-autoplay=` | Timed auto-advance; adds a toolbar widget |
| progress | init | `.impress-progress*` divs | Progress bar + counter |
| help | init | `#impress-help` div | Help popup; exposes `impress:help:add` event |
| blackout | init | `B`/`.` keys | Blank screen |
| fullscreen | init | `F5`/`Esc` | Fullscreen API |
| impressConsole | init | `P` key | Speaker console window; reads `.notes` |
| mobile | init | mobile UA | Adds `impress-mobile` + `next`/`prev` classes |
| resize | init | window resize | Re-fit via throttle |
| mouse-timeout | init | 3 s idle | Adds `impress-mouse-timeout` body class |
| toolbar | init | `#impress-toolbar` div | Shared widget host |
| form | init | automatic | `stopPropagation` on inputs; de-focus on leave |
| media | init | `data-media-*` | Per-step audio/video control |
| extras | init | extra `<script>`s | Auto-init 3rd-party add-ons (e.g. `mermaid.initialize()`) |

> [!NOTE]
> **rel** carries a documented backward-incompatible side effect: with it enabled, a missing `data-x/y/z` inherits from the previous step instead of defaulting to `0`. Its registration line (`impress.addPreInitPlugin( rel );`) can be commented out to disable the behavior without removing the file.

---

## 8. The Build Pipeline

The build is deliberately minimal — concatenation plus minification. `build.js` (run with `node build.js`):

1. **Concatenates** files in a fixed order into `js/impress.js`:
   - `src/impress.js` (core) first
   - then `src/lib/gc.js`, `src/lib/util.js`, `src/lib/rotation.js`
   - then all 22 plugins in `src/plugins/*/*.js` (explicit order list in `build.js`)
   - prepended with a generated header comment.
2. **Minifies** with Terser into `js/impress.min.js` + a source map `js/impress.min.js.map` (preserving `/^!/` license comments). These minified outputs are **not committed**.
3. **Auto-generates** `examples/index.html` — an `<ul>` linking every subdirectory under `examples/` (used for the gh-pages examples listing).

```bash
npm install        # installs build + test tooling
npm run build      # node build.js  -> regenerates js/impress.js (+ .min.js)
```

> [!IMPORTANT]
> Plugin **load order is defined explicitly in `build.js`**, not by directory glob alphabetization. If you add a plugin, add it to the `files.push(...)` list in `build.js`. Pre-init/pre-stepleave ordering at runtime is governed by the `weight` argument, but file order still affects which code registers first.

The build essentially does `cat src/impress.js src/lib/*.js src/plugins/*/*.js > js/impress.js`. The only npm packages it relies on are `buildify`/`ls`/`terser`; impress.js at runtime depends on nothing.

### npm scripts (`package.json`)

| Script | Command | Purpose |
|---|---|---|
| `build` | `node build.js` | Regenerate the bundle |
| `test` | `karma start --log-level debug --single-run=true` | Run tests headless via Karma |
| `lint` | `jshint src test/*.js && jscs src test/*.js` | Legacy linting |
| `new-lint` | `eslint src test` | ESLint |
| `all` | `npm run build && npm run test && npm run lint` | Full CI sequence |

### Dev requirements

- Node >= 7.6
- npm

---

## 9. Configuration Reference

impress.js has **no configuration file and no environment variables**. All configuration is HTML `data-*` attributes, resolved at runtime.

> [!NOTE]
> The single entry in the extracted `config_keys` data is `CHROME_BIN` (a `process.env` read in `karma.conf.js`) — that is a **test-harness** variable for pointing Karma at a Chrome binary, **not** a runtime setting of the library. There are no runtime env vars.

### Root configuration (defaults from `src/impress.js`)

| `data-*` attribute | Default | Maps to internal |
|---|---|---|
| `data-width` | `1920` | `config.width` |
| `data-height` | `1080` | `config.height` |
| `data-max-scale` | `3` | `config.maxScale` |
| `data-min-scale` | `0` | `config.minScale` |
| `data-perspective` | `1000` | `config.perspective` |
| `data-transition-duration` | `1000` | `config.transitionDuration` |

All other `data-*` attributes are consumed per-step by the core (`data-x/y/z`, `data-rotate*`, `data-scale`, `data-rotate-order`, per-step `data-transition-duration`) or by individual plugins (see the User Guide reference and Section 7).

### Test-harness configuration

`karma.conf.js` reads `process.env.CHROME_BIN` to locate a Chrome/Chromium binary for headless testing. `puppeteer` is a devDependency that can supply one.

---

## 10. Deployment & Hosting

Because a presentation is just static files, "deployment" is serving HTML/CSS/JS.

### Option A: Single self-contained directory

Ship your `index.html`, your CSS, a local `js/impress.js`, and any media. Open the file directly or serve it from any static host (GitHub Pages, Netlify, S3, nginx, etc.).

```text
my-talk/
├── index.html
├── css/
│   ├── impress-common.css   (optional)
│   └── my-style.css
├── js/
│   └── impress.js
└── images/
```

### Option B: CDN for the framework

Reference `https://cdn.jsdelivr.net/gh/impress/impress.js@2.0.0/js/impress.js` and host only your own HTML/CSS. Lighter to deploy, but requires network access at present time.

> [!TIP]
> Pin a specific `@version` in the CDN URL for reproducibility. The unversioned `.../impress.js/js/impress.js` always tracks the latest source and can change under you.

### Self-hosting the project / examples (gh-pages model)

The project's own demos are published to GitHub Pages (`http://impress.github.io/impress.js/`). `build.js` regenerates `examples/index.html` so the examples listing stays current. To self-host the demos, build and serve the repository root as static files.

> [!WARNING]
> No build step is required to *deploy a presentation* — only to regenerate `js/impress.js` from `src/`. End users serving a pre-built `js/impress.js` need no Node toolchain at all.

---

## 11. Security Analysis

impress.js is a client-side library with no server, no network calls, no authentication, no secrets, and no data persistence. Its attack surface is correspondingly small, but there are real considerations for anyone embedding it in a larger application.

### Threat model

| Concern | Assessment |
|---|---|
| Secrets / credentials | None. No keys, tokens, or `.env` in the runtime library. |
| Network / data exfiltration | None. The library makes no outbound requests. |
| Authentication / authorization | N/A. There is no access control to attack. |
| Input handling | The library reads `data-*` attributes and your DOM. It does **not** sanitize HTML — your slide content is rendered as-is. |
| DOM injection by plugins | Several plugins build HTML strings via `innerHTML` (e.g. the help plugin renders a `<table>` from registered rows). |

### Key findings

> [!CAUTION]
> **Untrusted content = XSS risk.** impress.js renders whatever HTML you put in your steps. If any part of a presentation is built from **user-supplied or remote content** (e.g. a CMS, comments, query parameters), you must sanitize it yourself before injecting it into a step. The framework provides no sanitization layer.

> [!WARNING]
> **Plugins use `innerHTML`.** The help plugin assembles its popup with `helpDiv.innerHTML = "<table>" + rows.join() + "</table>"`, where rows come from `impress:help:add` events. Any plugin that dispatches that event controls markup written to the DOM. In a trusted, author-controlled deck this is fine; if you let third parties register help text or write plugins, treat the help/toolbar text as a markup sink.

> [!NOTE]
> **The `data-goto`/`data-bookmark` key handling and URL hash routing** read values from the page/URL and `getElementById` them. They locate existing elements rather than executing strings, so they are not script-injection vectors, but a malicious hash simply navigates the deck — not a meaningful risk for a presentation.

> [!TIP]
> **`pointer-events` workaround.** `impress-common.css` sets `pointer-events: none` on `.impress-enabled` and re-enables it on `#impress` (and toolbar/console buttons) to work around a Chrome bug where Z-pushed elements fall behind `<body>`. This is a usability fix, not a security boundary, but be aware it makes everything outside `#impress` unclickable when applied.

### Hardening checklist (for embedding impress.js in an app)

- [ ] Sanitize any non-author content before placing it in a step (use a vetted sanitizer like DOMPurify).
- [ ] Don't expose the `impress:help:add` event surface to untrusted code.
- [ ] Serve over HTTPS and pin the CDN version if loading remotely.
- [ ] Apply a Content-Security-Policy on the hosting page; impress.js needs no `eval` or remote scripts of its own (it does not use `eval`).
- [ ] Audit any **extra add-ons** you include — they are third-party code outside the impress.js codebase.

---

## 12. Code Quality & Conventions

| Convention | Detail |
|---|---|
| Module pattern | Every file is an IIFE `( function(document, window){ "use strict"; ... } )(document, window)` |
| Style | ES5-style (`var`, function expressions); a few ES2015 touches (default params in `rotation.js`) |
| Naming | camelCase functions/vars; `data-*` attributes are kebab-case → `dataset` camelCase |
| Comments | Unusually rich, conversational inline comments — the source doubles as a teaching tool |
| Dependencies | Zero runtime deps by design; explicitly avoids jQuery and polyfills |
| Linting | Dual legacy (`jshint` + `jscs`) and modern (`eslint`) configs coexist (`.jshintrc`, `.jscsrc`, `.eslintrc.js`) |
| Browser API usage | Direct DOM APIs (`dataset`, `classList`, `querySelector`, `CustomEvent`, CSS transforms) |
| State management | Per-root closures; module-level registries for plugins/libraries |

> [!NOTE]
> The codebase predates ES modules and bundler ecosystems and stays intentionally framework-free. This is a deliberate design value ("showcase awesome CSS3 features... do not use jQuery or any other JavaScript libraries"), not technical debt.

### Extraction caveat for tooling

Static analysis tools that look for `function name(` / `class X` declarations will find **almost nothing** here — the entire API is built from function *expressions* assigned to `var`s and returned in object literals, inside IIFEs. The authoritative API surface is `DOCUMENTATION.md` plus the returned object in `src/impress.js` (the `{ init, goto, next, prev, swipe, tear, lib }` literal and the `impress.*` static assignments).

---

## 13. Testing Strategy

| Aspect | Detail |
|---|---|
| Frameworks | QUnit (assertions) + Syn (synthetic user input) + `qunit-assert-close` |
| Runner (headless) | Karma (`karma-chrome-launcher`, `karma-firefox-launcher`, `karma-qunit`), via `npm run test` |
| Runner (browser) | `qunit_test_runner.html` — open directly in Firefox/Chrome |
| Core tests | `test/core_tests.js`, `test/core_tests_presentation.html`, `test/helpers.js`, `test/non_default.*` |
| Plugin tests | Co-located per plugin (e.g. `test/plugins/rel/*` for the rel plugin: padding, rel-to, screen-size, rotation) |
| Headless Chrome | `karma.conf.js` reads `CHROME_BIN`; `puppeteer` can provide the binary |

> [!TIP]
> The maintainers note that running `firefox qunit_test_runner.html` is often **more informative** than `karma` (`npm run test`) — they run the same tests, but the browser runner gives clearer output. Use Karma for CI, the browser runner for debugging.

> [!NOTE]
> Test coverage is acknowledged as incomplete ("Yes, more tests are much welcome"). The `rel` plugin is the best-covered plugin; most other plugins have thin or no tests. This is the largest quality gap (see Improvements).

---

## 14. Writing a Plugin (extension recipe)

### A. Init plugin (most common)

Use this when your plugin reacts to navigation or adds UI/behavior.

```javascript
/* global document */
( function( document ) {
    "use strict";

    document.addEventListener( "impress:init", function( event ) {
        var api  = event.detail.api;   // the impress API for this root
        var gc   = api.lib.gc;         // ALWAYS route DOM/listeners through gc
        var util = api.lib.util;

        // Bind behavior using the public API
        gc.addEventListener( document, "keyup", function( e ) {
            if ( e.keyCode === /* your key */ 0 ) {
                api.next();
            }
        }, false );

        // Optionally register a help line
        util.triggerEvent( document, "impress:help:add",
            { command: "X", text: "What X does", row: 5 } );
    }, false );

} )( document );
```

> [!IMPORTANT]
> Route **every** element you add and **every** listener you bind through `api.lib.gc` (`gc.addEventListener`, `gc.appendChild`, `gc.pushElement`, `gc.pushCallback`). Otherwise `impress().tear()` will leave your plugin's DOM/listeners behind.

### B. Pre-init plugin (rewrite step attributes before init)

```javascript
( function( document, window ) {
    "use strict";
    var impress = window.impress;

    var myPreInit = function( root ) {
        // Read your custom data-* attrs and write data-x/y/z etc.
    };

    impress.addPreInitPlugin( myPreInit, 10 ); // weight: lower runs first
} )( document, window );
```

### C. Pre-stepleave plugin (redirect/abort transitions)

```javascript
( function( document, window ) {
    "use strict";
    var impress = window.impress;

    var myPreStepLeave = function( event ) {
        // event.detail.next, .reason, .transitionDuration are mutable
        // return false to ABORT the transition
        if ( /* condition */ false ) {
            return false;
        }
    };

    impress.addPreStepLeavePlugin( myPreStepLeave, 10 );
} )( document, window );
```

### D. Add it to the build

Append your file to the `files.push(...)` list in `build.js` (mind the order), then `npm run build`. Add a `README.md` in your plugin directory and a test under `test/plugins/yourplugin/`.

### Recipe checklist

- [ ] IIFE + `"use strict"`
- [ ] Choose the correct plugin type (init / pre-init / pre-stepleave)
- [ ] Use the public API + `api.lib.*` only — don't reach into core internals
- [ ] Route all DOM/listeners through `gc`
- [ ] Register help text if you bind a key
- [ ] Add to `build.js`, write a `README.md`, add tests
- [ ] Verify `tear()` cleans up your plugin

---

## 15. Potential Improvements & Tech Debt

Drawn from the source, the project's own READMEs, and the open GitHub issues.

### Release & packaging

- **Version inconsistency** between `package.json` (`1.1.0`) and the source/CDN (`2.0.0`). Reconcile and bump `package.json`.
- **Not published to npm** (issues #879, #653). A real npm package would let users `npm install` and enable bundler workflows. (#700 also asks for extra add-ons as npm dependencies.)
- Consider a proper release artifact that includes the minified `js/impress.min.js` (currently uncommitted).

### Architecture & features

- **Multiple-root support is half-hearted** — the code supports several `#impress` roots per page but warns it's not really intended; issue #664 proposes removing it. Decide: support it properly or drop it.
- **Hash-based routing clutters browser history** (#644) — each step pushes a history entry; `history.replaceState` would avoid the back-button pile-up.
- **Step-independent zoom/pan** (#649) is a requested capability the current camera model doesn't expose.
- **Resize transition hardcoded to 500 ms** (#682).
- **Scaling quality** on lower-than-config resolutions produces blurriness (#609) — a known CSS `scale` artifact.

### Mobile

- **Positioning is off on some mobile devices** (#844). The `mobile` plugin notes it doesn't account for non-linear (skip/goto) transitions.

### Code & docs

- **Form plugin is incomplete** — Tab navigation between form fields is unsupported because the navigation plugin must own the Tab key (flagged in the plugin's own README, "REQUIRES FURTHER DEVELOPMENT").
- **No JSDoc API documentation** (#662) — the API is documented in prose (`DOCUMENTATION.md`) but not machine-readable.
- **Documentation gaps**: add `gc` usage docs to `src/plugins/README.md` (#677); a media-plugin usage example (#689); a bug in GettingStarted was reported (#860, closed).
- **Thin test coverage** outside the `rel` plugin (acknowledged in README). Each plugin should get a `test/plugins/<name>/` suite.
- **Dual lint toolchains** (`jshint`+`jscs` legacy alongside `eslint`) should converge on ESLint and the legacy configs/scripts be retired.
- **A "Major Refactor"** is an open umbrella issue (#863).
- **No dedicated project webpage** (#645).

### Maintainability notes

- The build's explicit plugin list in `build.js` must be kept in sync manually when plugins are added/removed.
- `extras/` depends on a git submodule (`impress-extras`); a non-recursive clone silently omits it.

---

## 16. Quick Reference

### Build & test

```bash
npm install
npm run build      # node build.js -> js/impress.js (+ .min.js, .map)
npm run test       # karma headless
npm run new-lint   # eslint src test
npm run all        # build + test + lint
firefox qunit_test_runner.html   # browser test runner (more verbose)
```

### Core API (returned by `impress(rootId)`)

| Member | Type | Purpose |
|---|---|---|
| `init()` | method | Start the presentation |
| `goto(idx\|id\|el [, ms])` | method | Go to a step |
| `next()` / `prev()` | method | Sequential navigation |
| `swipe(pct)` | method | Semi-internal swipe animation (touch plugin) |
| `tear()` | method | Reset DOM to pre-init state |
| `lib` | object | `lib.gc`, `lib.util`, `lib.rotation` |

### Static members on `impress`

| Member | Purpose |
|---|---|
| `impress.supported` | Boolean feature-support flag |
| `impress.getConfig()` | Resolved config object |
| `impress.addLibraryFactory({name:fn})` | Register a library |
| `impress.addPreInitPlugin(fn, weight)` | Register a pre-init plugin |
| `impress.addPreStepLeavePlugin(fn, weight)` | Register a pre-stepleave plugin |

### Events (on the root element)

`impress:init` · `impress:stepenter` · `impress:stepleave` · `impress:steprefresh`

### Plugin coordination events

`impress:help:add` (register a help-popup line)

### Build order (from `build.js`)

```
src/impress.js
src/lib/{gc,util,rotation}.js
src/plugins/{autoplay,blackout,extras,form,fullscreen,goto,bookmark,help,
             impressConsole,media,mobile,mouse-timeout,navigation,navigation-ui,
             progress,rel,resize,skip,stop,substep,touch,toolbar}/*.js
```

### Key files

| File | Role |
|---|---|
| `src/impress.js` | Core engine (edit here) |
| `src/lib/*.js` | gc / util / rotation libraries |
| `src/plugins/*/` | 22 default plugins (+ READMEs) |
| `build.js` | Build (concat + minify + examples index) |
| `js/impress.js` | Generated bundle (don't edit) |
| `css/impress-common.css` | Optional sample plugin CSS |
| `DOCUMENTATION.md` | Reference API (authoritative) |
| `test/` | QUnit/Syn tests |

---

_Generated June 2026 from impress/impress.js at c9f6c67. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
