Skip to content

Engine API ​

API Reference: This Page · Types · Config Registry · Placement · Lightbox · Lens · Embeds · Coordinators · Storage · Events · Security · Servers

Core engine classes and helper functions. No DOM dependency — safe for Node.js.

Live version: https://docs.alap.info/api-reference/engine

Which import do I use? ​

Alap has two entry points. Pick the one that matches your environment:

  • alap — you're building a website. This gives you the engine, the UI, the web component, and the DOM layer. Use this when there's a browser.

  • alap/core — you're writing a server, a build tool, a test, or a library that processes Alap configs without rendering anything. No DOM, no window, no CSS. Use this when there's no browser.

Think of it this way: alap/core is the brain. alap is the brain plus the body. If you don't need hands and eyes (DOM, events, UI), just import the brain.

alap (browser) ​

Everything you need to render Alap on a page:

typescript
import {
  AlapEngine,
  ExpressionParser,
  mergeConfigs,
  AlapUI,
  AlapLinkElement,
  registerConfig,
  updateRegisteredConfig,
  defineAlapLink,
} from 'alap';

alap/core (no DOM) ​

Engine and utilities only. Safe for Node.js, tests, and build tools:

typescript
import {
  AlapEngine,
  ExpressionParser,
  mergeConfigs,
  validateConfig,
  sanitizeUrl,
  validateRegex,
} from 'alap/core';

AlapEngine ​

Stateful wrapper around the expression parser.

typescript
const engine = new AlapEngine(config);
MethodSignatureDescription
query()(expression: string, anchorId?: string) => string[]Expression to deduplicated array of item IDs
resolve()(expression: string, anchorId?: string) => ResolvedLink[]Sync: expression to full link objects (returns [] for unresolved async tokens)
resolveProgressive()(expression: string, anchorId?: string) => ProgressiveStateSync return with async side effect — see below
resolveAsync()(expression: string, anchorId?: string) => Promise<ResolvedLink[]>Awaits generate protocols, then returns resolved links
preResolve()(expressions: string[]) => Promise<void>Optional: warm the cache for generate protocols before first click
abortInFlight()(token: string) => voidCancel a currently-fetching protocol token (for cancelFetchOnDismiss)
getLinks()(ids: string[]) => ResolvedLink[]IDs to full link objects
clearGenerated()() => voidRemove all temp IDs injected by async resolution
updateConfig()(config: AlapConfig) => voidReplace configuration
clearCache()() => voidClear cached generate protocol results (and error cache)

Progressive resolution (3.2+) ​

resolveProgressive() is what the shipped renderers (AlapUI, AlapLens, AlapLightbox) call on trigger-click. It returns synchronously with whatever's available right now, and — as a side effect — starts any fetches that are still cold.

typescript
interface ProgressiveState {
  resolved: ResolvedLink[];   // static matches + already-cached async sources
  sources: SourceState[];     // one entry per async source that's loading, errored, or settled-empty
}

interface SourceState {
  token: string;                       // e.g. "hn:search:ai_papers"
  status: 'loading' | 'error' | 'empty';
  promise?: Promise<void>;             // only when status === 'loading'
  error?: Error;                       // only when status === 'error'
}

Re-invoke on each loading source's promise settlement to pick up the new state. Duplicate fetches for the same token collapse via an in-flight map; concurrency is capped by settings.maxConcurrentFetches (default 6) and each fetch is bounded by settings.fetchTimeout (default 30s).

typescript
const state = engine.resolveProgressive(':hn:top: | .coffee');
render(state.resolved);                       // show what we have
for (const src of state.sources) {
  if (src.status === 'loading') {
    src.promise!.then(() => {
      const fresh = engine.resolveProgressive(':hn:top: | .coffee');
      render(fresh.resolved);
    });
  }
}

Examples ​

typescript
import { AlapEngine } from 'alap/core';

const engine = new AlapEngine(config);

// Get just the IDs
const ids = engine.query('.coffee + .sf');
// → ['bluebottle']

// Get full link objects (sync — returns [] for cold async tokens)
const links = engine.resolve('.coffee');
// → [{ id: 'bluebottle', url: '...', label: 'Blue Bottle', tags: ['coffee', 'sf'] }, ...]

// Async: headless/programmatic resolution of generate protocols
const books = await engine.resolveAsync(':web:books:architecture:limit=5:');

// Optional: warm the cache ahead of time (e.g. on hover, for UX polish —
// shipped renderers don't need this, they handle async progressively)
await engine.preResolve([':web:books:', ':web:music:', ':atproto:feed:']);

// Update config at runtime
engine.updateConfig(newConfig);

ExpressionParser ​

Low-level stateless parser. Use AlapEngine unless you need direct parser access.

typescript
const parser = new ExpressionParser(config);
const ids = parser.query('.coffee + .nyc');

Utility functions ​

mergeConfigs(...configs) ​

Compose multiple configs. Later configs win on key collision.

typescript
const merged = mergeConfigs(baseConfig, overrides, moreOverrides);

Merges settings, macros, allLinks, searchPatterns independently. Returns a new object. Filters prototype-pollution keys (__proto__, constructor, prototype).

typescript
import { mergeConfigs } from 'alap/core';

const baseConfig = {
  settings: { menuTimeout: 5000 },
  allLinks: {
    home: { url: '/', label: 'Home', tags: ['nav'] },
    about: { url: '/about', label: 'About', tags: ['nav'] },
  },
};

const blogOverrides = {
  allLinks: {
    latest: { url: '/blog/latest', label: 'Latest Post', tags: ['blog'] },
  },
  macros: {
    navigation: { linkItems: '.nav' },
  },
};

const blogConfig = mergeConfigs(baseConfig, blogOverrides);
// blogConfig.allLinks has: home, about, latest
// blogConfig.macros has: navigation
// blogConfig.settings has: menuTimeout: 5000

validateConfig(raw) ​

Sanitize an untrusted config (from a remote API or JSON file).

typescript
const config = validateConfig(JSON.parse(jsonString));
  • Validates structure (allLinks must be an object, links must have url strings)
  • Sanitizes URLs via sanitizeUrl()
  • Removes dangerous regex patterns via validateRegex()
  • Filters prototype-pollution keys
  • Strips unknown fields from link entries
  • Returns a sanitized copy. Throws if structurally invalid.
typescript
import { validateConfig } from 'alap/core';

const response = await fetch('https://api.example.com/configs/main');
const raw = await response.json();

try {
  const config = validateConfig(raw);
  registerConfig(config);
} catch (e) {
  console.error('Invalid config:', e.message);
}

sanitizeUrl(url) ​

Block dangerous URI schemes.

typescript
sanitizeUrl('https://example.com')     // → 'https://example.com'
sanitizeUrl('javascript:alert(1)')      // → 'about:blank'
sanitizeUrl('data:text/html,...')       // → 'about:blank'
sanitizeUrl('/relative/path')           // → '/relative/path'
sanitizeUrl('mailto:user@example.com')  // → 'mailto:user@example.com'

Allows: http, https, mailto, tel, relative URLs. Blocks: javascript, data, vbscript, blob.

validateRegex(pattern) ​

Check a regex pattern for catastrophic backtracking (ReDoS).

typescript
validateRegex('(a+)+$')   // → { safe: false, reason: 'Nested quantifier detected...' }
validateRegex('bridge')    // → { safe: true }
validateRegex('\\w+')      // → { safe: true }

Shared utilities ​

Internal building blocks used by all adapters. Exported for custom integrations.

Pure DOM builder. Returns a <ul> or <ol> element.

OptionTypeDescription
listTypestring'ul' or 'ol'
maxVisibleItemsnumberScroll after N items
globalHooksstring[]Default hooks from settings.hooks
liAttributesRecord<string, string>Extra attributes on <li>
aAttributesRecord<string, string>Extra attributes on <a>

handleMenuKeyboard(event, items, activeElement, closeMenu, options?) ​

Stateless keyboard nav handler. Returns true if the event was consumed.

DismissTimer ​

Auto-dismiss timer. Call start() when mouse leaves, stop() when it re-enters.

typescript
const timer = new DismissTimer(5000, () => { menu.hidden = true; });

menu.addEventListener('mouseenter', () => timer.stop());
menu.addEventListener('mouseleave', () => timer.start());

resolveExistingUrlMode(trigger, globalSetting) ​

Resolves the existingUrl mode for a trigger element.

Extracts href from a trigger and injects a synthetic link item. Returns a new array.

Building a custom adapter ​

These utilities compose into a custom adapter in ~30 lines. See Events for the full event type definitions.