WASM API

The npm package @stormlightlabs/lectito exposes Lectito to JavaScript through wasm-bindgen.

It supports browser, web worker, bundler, and Node.js use.

npm install @stormlightlabs/lectito

The Rust crate is still named lectito-wasm.

Build Targets

wasm-pack build crates/wasm --target bundler
wasm-pack build crates/wasm --target web
wasm-pack build crates/wasm --target nodejs

wasm-pack writes lectito_wasm.d.ts with the public TypeScript API.

Initialization

Bundler builds initialize when imported:

import { extract } from "@stormlightlabs/lectito";

const article = extract(html, "https://example.com/post");

The web target needs the async initializer:

import init, { extract } from "./lectito_wasm.js";

await init();

const article = extract(html, "https://example.com/post");

The nodejs target initializes when loaded:

const { extract } = require("./lectito_wasm.js");

const article = extract(html, "https://example.com/post");

Functions

export function extract(
  html: string,
  baseUrl?: string | null,
  options?: ReadabilityOptions | null,
): Article | null;

export function extractWithDiagnostics(
  html: string,
  baseUrl?: string | null,
  options?: ReadabilityOptions | null,
): ExtractionReport;

export function isProbablyReadable(html: string, options?: ReadableOptions | null): boolean;

export function cleanHtml(
  html: string,
  baseUrl?: string | null,
  options?: CleanHtmlOptions | null,
): string | null;

export function htmlToMarkdown(html: string): string;

export function markdownToHtml(markdown: string, options?: MarkdownOptions | null): string;

export function analyzeTropes(text: string, options?: TropeOptions | null): TropeReport;

Types

Option fields use camelCase. Returned article fields keep the core Rust snake_case names.

export type MediaRetention = "none" | "conservative" | "article" | "all";

export interface ReadabilityOptions {
  maxElemsToParse?: number | null;
  nbTopCandidates?: number;
  charThreshold?: number;
  contentSelector?: string | null;
  siteProfiles?: string[];
  mobileViewportWidth?: number | null;
  classesToPreserve?: string[];
  keepClasses?: boolean;
  disableJsonLd?: boolean;
  linkDensityModifier?: number;
  mediaRetention?: MediaRetention;
}

export interface ReadableOptions {
  minContentLength?: number;
  minScore?: number;
}

export interface MarkdownOptions {
  gfm?: boolean;
  footnotes?: boolean;
  math?: boolean;
  allowRawHtml?: boolean;
}

export type TropePreset = "lenient" | "balanced" | "strict";

export type TropeCategory =
  | "word_choice"
  | "sentence_structure"
  | "paragraph_structure"
  | "tone"
  | "formatting"
  | "composition";

export type TropeSignal = "low" | "medium" | "high" | "very_high";

export type Severity = "low" | "medium" | "high";

export interface TropeOptions {
  preset?: TropePreset;
  minSeverity?: Severity;
  includeSuggestions?: boolean;
}

export interface TextRange {
  start_byte: number;
  end_byte: number;
  start_utf16: number;
  end_utf16: number;
  start_line: number;
  start_column: number;
  end_line: number;
  end_column: number;
}

export interface RuleSource {
  file: string;
  heading: string;
}

export interface TropeFinding {
  rule_id: string;
  category: TropeCategory;
  severity: Severity;
  range: TextRange;
  matched_text: string;
  message: string;
  suggestion: string | null;
  source: RuleSource | null;
}

export interface TropeSummary {
  word_count: number;
  finding_count: number;
  findings_per_1000_words: number;
  category_counts: Partial<Record<TropeCategory, number>>;
  severity_counts: Partial<Record<Severity, number>>;
  top_categories: TropeCategory[];
  repeated_rules: string[];
}

export interface TropeReport {
  score: number;
  signal: TropeSignal;
  findings: TropeFinding[];
  summary: TropeSummary;
}

export type CleanHtmlOptions = ReadabilityOptions;

export interface Article {
  title?: string | null;
  byline?: string | null;
  dir?: string | null;
  lang?: string | null;
  content: string;
  markdown: string;
  text_content: string;
  length: number;
  excerpt?: string | null;
  site_name?: string | null;
  published_time?: string | null;
  image?: string | null;
  domain?: string | null;
  favicon?: string | null;
}

export interface ExtractionReport {
  article: Article | null;
  diagnostics: unknown;
}

mediaRetention accepts "none", "conservative", "article", or "all".

Trope diagnostics

import { analyzeTropes } from "@stormlightlabs/lectito";

const report = analyzeTropes("It is worth noting that this serves as an example.");
console.log(report.signal, report.findings);

analyzeTropes returns heuristic writing diagnostics. It reports matched phrases and structures; it does not determine authorship or prove that a person or model wrote the text.

minSeverity filters findings at the requested severity or higher. includeSuggestions controls the nullable suggestion field. The default preset is "balanced". The "lenient" preset removes low-severity findings and requires stronger repeated evidence. The "strict" preset includes lower-evidence structural and document findings. Report ranges include both byte and UTF-16 offsets so callers can decorate JavaScript strings without converting the input first.

The source field points to the vendored tropes.md catalog and its source heading. Custom rules are not supported by the current WASM API; it uses the bundled rule catalog only.

Errors

Functions throw JavaScript Error objects for invalid base URLs, oversized documents, option conversion failures, and serialization failures.

Sanitization

cleanHtml performs Lectito article cleanup. It is not a complete untrusted-HTML security policy.

Browser integrations that accept arbitrary HTML should run a dedicated sanitizer such as DOMPurify before passing content into Lectito. Sanitize again before rendering returned HTML when the original input is untrusted.

Release Checks

Run the WASM tests and build all supported package targets:

pnpm --dir packages/web exec wasm-pack test --node ../../crates/wasm
pnpm --dir packages/web exec wasm-pack build ../../crates/wasm --target bundler --out-dir ../../target/wasm-pack/bundler
pnpm --dir packages/web exec wasm-pack build ../../crates/wasm --target web --out-dir ../../target/wasm-pack/web
pnpm --dir packages/web exec wasm-pack build ../../crates/wasm --target nodejs --out-dir ../../target/wasm-pack/nodejs

The build commands run wasm-opt; restricted sandboxes may need permission to execute it.