{}tiptap-model-language
A Tiptap extension · powered by model-language

Type-safe templates
right inside your editor

Variables, conditions, loops and filters that render to clean text, with live validation, quick-fixes and staged autocomplete.

$npm i tiptap-model-language
Try it live ↓

Everything you'd expect from a typed language

…now living inside your rich-text editor.

🔤

Variables & filters

{{ contact.first_name | default: "there" | upper }}

Dotted paths, chained filters (default, upper, trim, date, round, currency…), and nullable-aware fallbacks so you never render a blank.

🔀

Conditions & loops

{{if contact.vip}}{{else}}-{{/if}}

if · elseif · else · for … else with JavaScript-style lexical block scope. A block opened in a branch must close in that same branch.

🩺

Local validation

ML210 'First name' can be empty…

The model-language validate() engine runs in-process on every keystroke: unknown fields, missing defaults, unbalanced blocks. Zero round-trips.

🛠️

Quick-fixes

conta -> contains · + | default

Hover a squiggle for a one-click fix: add a type-aware default, insert the missing {{/if}} in the right scope, or correct a mistyped operator.

⌨️

Staged autocomplete

{{ contact. == "high"

field, then operator, then value, step by step. Enums list options, dates offer format presets with a live preview, timezones from a dropdown.

🌍

i18n & theming

labels: { addDefault: "Додати" }

Override every string for localization, toggle diagnostic severities, and style with your own Tailwind classes. Light and dark themes included.

Three moving parts

01
🗂️

Pass a typed schema

The host hands the extension a FieldSchema (dotted paths, types, nullability and enum options) via a prop or setModelData. It's the single source of truth for autocomplete and validation.

02
✍️

Author in the editor

Writers get VS Code-style feedback while typing: syntax highlighting, staged autocomplete, hover diagnostics and one-click quick-fixes. Broken tags and empty fields are caught at edit time.

03
🚀

Render clean text

The very same model-language engine renders the template against live data, so what you validate is exactly what ships: clean output, never leaked syntax, never a blank.

Built for teams that render dynamic text against live data: AI agent prompts, transactional email, chat and notification templates, no-code message builders. Framework-agnostic, offline-capable, fully themeable, and impossible to leak raw syntax from.

Up and running in three steps

Drop it into any React + Tiptap app. Copy, paste, done.

$ npm i tiptap-model-language @tiptap/react @tiptap/starter-kit
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { ModelSyntax } from "tiptap-model-language";

// 1. your org's field schema (types, nullability, enum values)
const schema = [
  { path: "contact.first_name", type: "string", nullable: true },
  { path: "contact.priority", type: "enum", values: ["high", "low"] },
];

// 2. namespaces power the {{ autocomplete (grouped by prefix)
const namespaces = [
  { key: "contact", label: "Contact", fields: [
    { key: "first_name", type: "string", label: "First name" },
    { key: "priority", type: "enum", label: "Priority",
      values: ["high", "low"] },
  ]},
];

export function TemplateEditor() {
  const editor = useEditor({
    extensions: [
      StarterKit,
      // 3. pass schema + namespaces straight in (static case)
      ModelSyntax.configure({
        namespaces,
        schema,
        onResult: (r) => console.log(r.diagnostics, r.maxTokenEstimate),
      }),
    ],
  });

  // Schema loads async or changes at runtime? Push it live instead:
  //   editor.commands.setModelData({ namespaces, schema })

  return <EditorContent editor={editor} />;
}

Want every prop and command? Read the docs