Strategies for representing functions as inline tokens

It’s pretty common to represent @mentions and #tags as atomic tokens with autocomplete in ProseMirror, and I want to compose these same primitives to build a higher-level structured editor that can support calling functions with arguments.

Let’s consider the simplest example of a unary function: call(Sam).

Suppose we have some atomic inline tokens for both the function name and the mention. Something like this:

// A typical atomic token
const func: NodeSpec = {
	group: "inline",
	inline: true,
	atom: true,
	attrs: { func: { default: "" } },
	selectable: false,
	draggable: false,
	toDOM: (node) => {
		const span = document.createElement("span")
		span.setAttribute("data-func", node.attrs.func)
		span.innerText =  node.attrs.func
		return span
	parseDOM: [
			tag: `span[data-func]`,
			getAttrs: (dom) => {
				if (dom instanceof HTMLElement) {
					var value = dom.getAttribute("data-func")
					return { func: value }

Now the question is: how do we compose all of this together into something that can display the wrapping parentheses all as one token.

My plan is to create another inline token that composes these together into another inline token.

// This is how I want to compose them together:
const pair: NodeSpec = {
	content: "func (text|mention)*",
	inline: true,
	selectable: false,
	draggable: false,
	toDOM: () => {
		return ["span", { class: "pair" }, 0]
	parseDOM: [{ tag: "span.pair" }],

This way, the function and the arguments are grouped together. In theory, I just have to wrap the func node in a pair node and I can edit inside the arguments section by moving my cursor around:

Ideally that red cursor location would not be allowed, but we can coerce the selection to make that happen.

What do you think of this approach? Any suggestions on how to implement something like this?

I just discovered some good reading on this topic:

It sounds like creating a custom NodeView might be the way to go…

So for some reason, I’m not able to create a this pair node wrapper. When I create a func, I try to wrap it in a pair and replace the range.

const node = view.state.schema.nodes.pair.create({}, [
	view.state.schema.nodes.func.create({func: value})

const tr =

This doesn’t work though. There’s no errors and I can see the func node render, but the pair node was ignored.

You can inspect nodes by converting them to string. That might help figure out what the transaction is doing. Maybe the context where the node is being inserted doesn’t allow pair nodes?

So it turns out, I was missing a group: "inline" which was causing it not to work.

I’ve figured something out using an approach similar to your footnote example.