Skip to main content

File Formats

FileFormat and TextFileFormat let a Luau script claim custom file extensions in the Rive editor. They are editor extension protocols: they define import, analysis, and preview behavior. They are separate from runtime Node, Layout, Converter, and listener protocols.

Evidence tier

The signatures on this page match the current public Rive docs and the current editor/LSP FileFormat declarations. They are not C++ runtime-v0.1.344 Context methods. The exact Web 2.42.0 target consumes exported text documents as Blob assets through context:blob(name).

Choosing a protocol

ProtocolImported documentEditor behaviorExported asset
FileFormatBinary bytesOptional custom previewBlob with original bytes
TextFileFormatTextEditable, collaborative document plus optional language intelligence and previewPlain Blob read with context:blob(name)

Both protocols use a zero-argument factory and require name and extensions fields. Extensions omit the leading dot.

The protocol implementation is authored script code. The imported document is a separate asset: its editor callbacks receive FormatDocument, while an exported runtime receives a Blob. Neither a serialized ScriptModuleAsset nor its language/handle metadata becomes a FileFormat global or module name.

Minimal binary format

local format: FileFormat = {
name = "Model",
extensions = { "glb" },
}

return function(): FileFormat
return format
end

Minimal text format

local format: TextFileFormat = {
name = "Notes",
extensions = { "note" },
}

function format.highlight(
_self: TextFileFormat,
doc: FormatDocument,
_parsed: buffer?
): { FormatToken }
if doc.text == nil or #doc.text == 0 then
return {}
end

return {{
line = 0,
column = 0,
length = #doc.text,
scope = "string",
}}
end

return function(): TextFileFormat
return format
end

The callback uses a typed table method, so Rive passes the format table as self. Analysis callbacks should depend only on the supplied document and the optional parsed buffer. For TextFileFormat, type self as TextFileFormat in each callback.

Shared fields and callbacks

Fields

name: string
extensions: { string }

parse(doc)

parse(self: FileFormat, doc: FormatDocument) -> buffer?

Optional. Rive calls it once per document version and caches the returned buffer by content. The cached value is passed to the other callbacks. Keep it as derived data rather than hidden mutable view state.

view(doc, editor, surface, parsed)

view(
self: FileFormat,
doc: FormatDocument,
editor: EditorContext,
surface: FormatSurface,
parsed: buffer?
) -> FormatView?

Optional. Each open surface receives a separate view. FormatSurface.kind is "pane" | "inspector"; "pane" covers both the editor split and a full viewer tab. A document can have several surface instances at once.

Text analysis callbacks

TextFileFormat adds these optional callbacks:

highlight(self: TextFileFormat, doc: FormatDocument, parsed: buffer?) -> { FormatToken }
diagnostics(self: TextFileFormat, doc: FormatDocument, parsed: buffer?) -> { FormatDiagnostic }
completions(self: TextFileFormat, doc: FormatDocument, line: number, column: number, parsed: buffer?) -> { FormatCompletion }
hover(self: TextFileFormat, doc: FormatDocument, line: number, column: number, parsed: buffer?) -> FormatHover?
format(self: TextFileFormat, doc: FormatDocument, parsed: buffer?) -> string?

format returns the complete replacement text or nil for no change. Rive computes and applies minimal edits. The analysis callbacks are pure; keep surface-specific animation, scroll, and interaction state in FormatView.

FormatDocument

Exactly one of text and bytes is present.

FieldTypeMeaning
namestringDisplay name without relying on an asset ID
extensionstringExtension without the dot
textstring?Full text for a text document
bytesbuffer?Raw bytes for a binary document

Token, diagnostic, completion, and hover positions are zero-based. Document positions returned to Rive use byte offsets; FormatView.selectionChanged reports zero-based code-point line and column values.

FormatSurface

export type FormatSurface = {
kind: "pane" | "inspector",
}

"pane" covers the editor split and a full viewer tab. "inspector" is the selected asset strip. There is no third "viewer" kind.

Tokens, diagnostics, completions, and hover

export type FormatToken = {
line: number,
column: number,
scope: FormatScope,
length: number?,
}

export type FormatDiagnostic = {
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
message: string,
severity: ("error" | "warning")?,
}

export type FormatCompletion = {
text: string,
startLine: number?,
startColumn: number?,
endLine: number?,
endColumn: number?,
}

export type FormatHover = {
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
preview: string?,
documentation: string?,
}

Current FormatScope values are:

"none" | "keyword" | "type" | "literal" | "number" | "operator"
| "punctuation" | "property" | "string" | "comment" | "boolean"
| "nil" | "interp" | "function"

FormatView

draw is the view's required rendering callback. Other callbacks are optional.

draw(self: FormatView, renderer: Renderer, width: number, height: number) -> ()
measure(self: FormatView, viewportWidth: number, viewportHeight: number) -> (number, number)
advance(self: FormatView, seconds: number) -> boolean
textScrolled(self: FormatView, x: number, y: number, line: number, maxLine: number) -> ()
paneScrolled(self: FormatView, x: number, y: number) -> ()
paneWheel(self: FormatView, dx: number, dy: number) -> ()
pointerDown(self: FormatView, event: PointerEvent) -> ()
pointerMove(self: FormatView, event: PointerEvent) -> ()
pointerUp(self: FormatView, event: PointerEvent) -> ()
documentChanged(self: FormatView, doc: FormatDocument, parsed: buffer?) -> ()
selectionChanged(self: FormatView, line: number, column: number) -> ()
dispose(self: FormatView) -> ()

measure drives pane scrollbars. Omit it for a fit-to-viewport or internally scrolling view. Return true from advance only while the view needs animation frames, and release per-surface resources in dispose.

EditorContext

EditorContext is scoped to a FileFormat view. Asset and artboard handles are immutable or private to that view; they do not expose the open file's live state.

editor:image(name: string) -> Image?
editor:blob(name: string) -> Blob?
editor:artboard(name: string) -> Artboard<ViewModel?>?
editor:hostArtboard(name: string) -> Artboard<ViewModel?>?
editor:theme() -> EditorTheme
editor:openUrl(url: string)
editor:themeFont() -> Font?
editor:defaultFont() -> Font?
editor:codeFont() -> Font?
editor:editorScroll() -> EditorScroll?
editor:scrollTo(x: number, y: number) -> ()
editor:scrollEditorTo(line: number) -> ()
editor:requestDraw() -> ()

Current public docs additionally list shader, canvas, gpuCanvas, features, and decodeImage. The live built-in reference read for this audit ended at requestDraw, so treat those later GPU/image helpers as public-docs evidence until the selected editor exposes them.

artboard resolves relative to the format script's file. hostArtboard always resolves in the file being edited. This distinction matters when the format ships in a library.

EditorTheme and EditorScroll data

editor:theme() returns current theme values. Read them during draw or measure rather than caching them across frames.

EditorTheme fieldType
scopes{ [FormatScope]: EditorThemeStyle }
background, divider, dividerHighlightColor
error, warning, lineNumberColor
fontSize, lineHeightnumber

EditorThemeStyle has color: Color and optional weight: number?.

editor:editorScroll() returns EditorScroll? with these read-only fields:

FieldTypeMeaning
percentnumberScroll position from 0 to 1
linenumberFractional zero-based top line
maxLinenumberTop line at the end of the scroll range
cursorLinenumber?Primary cursor line, when present
cursorFractionnumber?Cursor position within or outside the viewport

Runtime handoff through Blob

Text documents export as ordinary Blob assets. Runtime scripts do not receive FormatDocument or EditorContext; they resolve the exported asset:

local document = context:blob("ReleaseNotes")
if document then
if document.size == 0 then
print("")
else
local data = document.data
if data then
print(buffer.tostring(data))
end
end
end

At runtime-v0.1.344, Blob exposes only name, size, and data; Blob:asString() does not exist. data is nil for a zero-byte Blob, so check size or guard data before calling buffer.tostring.

Test the editor extension and exported Blob path separately. A functioning highlighter does not prove that the chosen runtime/export contains the expected asset name or bytes.

Boundaries

  • Do not expose serialized host metadata or internal asset IDs to Luau code.
  • Use require("Name") for script modules and context:blob("Name") for exported documents.
  • Keep per-view state in each FormatView; do not hide it in pure analysis callbacks.
  • Treat FileFormat callbacks as current editor contracts, then validate the runtime Blob consumer in the exact release lane.

See also