<div class="docs-content" data-render>
# Frontend and components
The client side is vanilla JavaScript that enhances server-rendered HTML in place. This page covers the no-framework rule, the `Application` root, the custom web components, the content rendering pipeline, and the declarative `data-*` contract between markup and controllers. See also [Styling and templates](/docs/architecture-styling.html).
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
## Vanilla JavaScript, no framework
All JavaScript lives in `static/js/` as **ES6 modules, one class per file**. There is no React, Vue, or Angular, no NPM, and no bundler: the browser loads native modules directly. A small set of libraries is loaded from `static/vendor/` over CDN-style script tags: `marked` (markdown), `highlight.js` (syntax highlighting), `DOMPurify` (sanitization), and `emoji-picker-element`.
Those vendor scripts MUST be loaded with `defer` or `type="module"`. The Playwright tests wait on `domcontentloaded`; a blocking script would make that event time out.
## The Application root
`Application.js` is the browser-side root. A single instance is created and exposed globally:
```javascript
const app = new Application();
window.app = app;
```
Its constructor imports `components/index.js` (which defines every web component) and instantiates every page controller as a property (`app.modals`, `app.forms`, `app.votes`, `app.comments`, and so on). It also creates the three component singletons once and exposes them as `app.dialog`, `app.contextMenu`, and `app.toast`. Page code reaches any capability through `window.app`.
## Custom web components
Reusable UI widgets are custom elements with the `dp-` prefix, living in `static/js/components/`. Each extends a thin `Component` base that adds typed attribute helpers:
```javascript
export class Component extends HTMLElement {
attr(name, fallback = "") { ... }
boolAttr(name) { ... }
intAttr(name, fallback = 0) { ... }
}
```
Every component **renders into the light DOM (no shadow root)** so the global CSS applies, and **self-registers** with `customElements.define` at the bottom of its file. `components/index.js` imports them all and is imported by `Application.js`, so every element is defined site-wide. The catalog:
| Element | Purpose |
|---------|---------|
| `dp-avatar` | Renders a user avatar. |
| `dp-code` | Renders a syntax-highlighted code block. |
| `dp-content` | Wraps the content renderer behind `data-render`. |
| `dp-upload` | The single file-upload button used everywhere. |
| `dp-toast` | Transient notifications (`app.toast`). |
| `dp-dialog` | Confirm / prompt / alert modals (`app.dialog`). |
| `dp-context-menu` | Right-click and long-press menus (`app.contextMenu`). |
**The component-vs-module rule.** Self-contained presentational widgets become components. Partial-bound controllers that enhance server-rendered markup (votes, reactions, comments) and pure utilities stay plain modules, because they augment existing HTML rather than render standalone UI.
## Content rendering pipeline
`ContentRenderer.js` runs on every element marked `data-render`, in this exact order:
1. emoji shortcode -> unicode
2. markdown via `marked`
3. **`DOMPurify.sanitize`**
4. `highlight.js` on `<pre><code>`
5. image URL -> `<img>`
6. YouTube URL -> trusted iframe
7. autolink remaining URLs
A `NodeIterator` skips `CODE`, `PRE`, `SCRIPT`, and `STYLE` so URLs inside code are never rewritten. Sanitization runs on the raw `marked` output before the trusted YouTube embeds are injected, so user content cannot inject script, event-handler, or iframe payloads while our own embeds survive.
This step is **fail-closed**: `render()` throws if `DOMPurify` is not loaded rather than emitting unsanitized HTML. Because content is rendered client-side from `element.textContent`, Jinja autoescaping alone does not protect it. This sanitize step is the XSS control.
## The data-* contract
Controllers find and enhance markup through declarative `data-*` attributes rather than inline scripts. This keeps behavior HTML-driven and auditable (grep for the attribute):
| Attribute | Behavior |
|-----------|----------|
| `data-render` | Render markdown content through the pipeline. |
| `data-modal` | Open the named `.modal-overlay`. |
| `data-confirm` | Show a confirm dialog before the action. |
| `data-copy` | Copy the referenced content to the clipboard. |
| `data-share` | Copy a share URL. |
| `data-mention` | Attach `@mention` autocomplete to a textarea. |
</div>