Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/fresh-file-observe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lix-js/sdk": patch
---

Fix observe invalidation for file queries so file reads re-emit when state commits touch the same file, preventing stale data from being served.
5 changes: 5 additions & 0 deletions .changeset/quiet-seas-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lix-js/plugin-prosemirror": patch
---

Fix handling of empty ProseMirror documents by avoiding JSON parse on empty file data.
5 changes: 5 additions & 0 deletions .changeset/spotty-plums-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lix-js/plugin-json": patch
---

improved readme
5 changes: 5 additions & 0 deletions .changeset/vast-geckos-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lix-js/plugin-md": patch
---

improved readme
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,20 @@ export function CodeBlock({
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
aria-label={isExpanded ? "Collapse code block" : `Expand code block (${lineCount} lines)`}
aria-label={
isExpanded
? "Collapse code block"
: `Expand code block (${lineCount} lines)`
}
>
{isExpanded ? (
<>
Show Less <ChevronUp className="h-3 w-3" />
</>
) : (
<>
Show More ({lineCount} lines) <ChevronDown className="h-3 w-3" />
Show More ({lineCount} lines){" "}
<ChevronDown className="h-3 w-3" />
</>
)}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,11 @@ export function MessageBody({ content }: { content: string }) {
return <MarkdownImage src={src} alt={alt} />;
},
del({ children }) {
return <del className="line-through text-muted-foreground">{children}</del>;
return (
<del className="line-through text-muted-foreground">
{children}
</del>
);
},
input({ checked, ...props }) {
return (
Expand Down
88 changes: 60 additions & 28 deletions packages/lix/plugin-json/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,62 @@
# Lix Plugin `.json`

This plugin adds support for `.json` files in Lix.

## Limitations

The JSON plugin does not track changes within objects in arrays. Instead, it treats the entire array as a single entity.

- The JSON plugin does not require a unique identifier for objects within arrays.
- Any change within an array will be detected as a change to the entire array.
- The plugin does not differentiate between modifications, deletions, or insertions within arrays.

### Valid Example

```json
{
"apiKey": "your-api-key-here",
"projectId": "your-project-id-here",
"settings": {
"notifications": true,
"theme": "dark",
"autoUpdate": false
},
"userPreferences": {
"language": "en",
"timezone": "UTC"
}
}
# Lix Plugin `.json`

Plugin for [Lix](https://lix.dev) that adds support for `.json` files.

## Installation

```bash
npm install @lix-js/sdk @lix-js/plugin-json
```

## Quick start

```ts
import { openLix, newLixFile } from "@lix-js/sdk";
import { plugin as jsonPlugin } from "@lix-js/plugin-json";

const lixFile = await newLixFile();
const lix = await openLix({
blob: lixFile,
providePlugins: [jsonPlugin],
});

// Insert a JSON file
const file = await lix.db
.insertInto("file")
.values({
path: "/config.json",
data: new TextEncoder().encode(
JSON.stringify({ apiKey: "abc", features: { search: true } }, null, 2),
),
})
.returningAll()
.executeTakeFirstOrThrow();

// Update the file later — the plugin will detect pointer-level changes
await lix.db
.updateTable("file")
.set({
data: new TextEncoder().encode(
JSON.stringify(
{ apiKey: "abc", features: { search: false, ai: true } },
null,
2,
),
),
})
.where("id", "=", file.id)
.execute();
```

## How it works

- JSON Pointer granularity: every leaf value is addressed by its pointer (for example `/features/search`), and each pointer becomes an entity in Lix.
- Object diffing: property additions, edits, and deletions are tracked independently per key.
- Array handling: array items are addressed by index. Insertions or reorderings create multiple changes because indices serve as the identity.
- Apply phase: `applyChanges` patches the parsed JSON with the incoming pointer changes and writes a serialized JSON document back to the file.

## Limitations and tips

- Arrays are position-based. Reordering items or inserting into the middle will appear as several deletions/insertions.
- The plugin expects valid JSON input; invalid JSON cannot be parsed or diffed.
- Large, deeply nested objects are supported, but providing stable array ordering reduces noisy diffs.
57 changes: 55 additions & 2 deletions packages/lix/plugin-md/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# Lix Plugin `.md`
# Lix Plugin `.md`

This plugin adds support for `.md` files in Lix.
Plugin for [Lix](https://lix.dev) that tracks changes in Markdown files.

It parses Markdown into the [`@opral/markdown-wc`](https://www.npmjs.com/package/@opral/markdown-wc) AST, tracks top-level blocks as entities, and renders rich diffs via [HTML Diff](https://html-diff.lix.dev/). The plugin follows the [markdown-wc spec](https://markdown-wc.opral.com/), giving Lix a stable Markdown shape to detect, apply, and render changes against.

## Installation

```bash
npm install @lix-js/sdk @lix-js/plugin-md
```

## Quick start

```ts
import { openLix, newLixFile } from "@lix-js/sdk";
import { plugin as markdownPlugin } from "@lix-js/plugin-md";

const lixFile = await newLixFile();
const lix = await openLix({ blob: lixFile, providePlugins: [markdownPlugin] });

// Insert a Markdown file
const file = await lix.db
.insertInto("file")
.values({
path: "/notes.md",
data: new TextEncoder().encode(`# Heading\n\nFirst paragraph.`),
})
.returningAll()
.executeTakeFirstOrThrow();

// Update the file later on — the plugin will detect block-level changes
await lix.db
.updateTable("file")
.set({
data: new TextEncoder().encode(
`# Heading\n\nFirst paragraph.\n\nNew note.`,
),
})
.where("id", "=", file.id)
.execute();
```

## How it works

- Block-level entities: Each top-level mdast node (paragraphs, headings, lists, tables, code blocks, etc.) is stored as its own entity. The root entity keeps the ordering of those blocks.
- Stable IDs without markup: IDs are minted automatically and kept out of the serialized Markdown, so you do not need to add markers to your documents.
- Nested awareness: Nested nodes (list items, table cells, inline spans) get ephemeral IDs during diffing to align edits but are not persisted as separate entities.
- Similarity-based matching: The detector uses textual similarity and position hints to decide whether a block was edited, moved, inserted, or deleted, even when headings or paragraphs change slightly.
- Apply & render: `applyChanges` rebuilds Markdown from stored snapshots, and `renderDiff` produces an HTML diff (using `@lix-js/html-diff`) that highlights before/after content with `data-diff-key` markers.

## Limitations and tips

- Changes are tracked per top-level block; inline-level differences are aggregated into the parent block.
- Replacing an entire block with unrelated content will be treated as a delete + insert instead of a modification.
- Large documents are supported, but providing reasonably distinct headings/paragraphs improves block matching when content is rearranged.
100 changes: 61 additions & 39 deletions packages/lix/plugin-prosemirror/README.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,78 @@
# Lix Plugin Prosemirror
# Lix Plugin ProseMirror

This package enables change control for [ProseMirror](https://prosemirror.net/) documents using the Lix SDK.
Plugin for [Lix](https://lix.dev) that tracks changes in [ProseMirror](https://prosemirror.net/) documents.

[![Screenshot of the ProseMirror example app](./assets/prosemirror.png)](https://prosemirror-example.onrender.com/)

[Try out the example app →](https://prosemirror-example.onrender.com/)

An example can be found in the [example](./example) directory.
[Try out the example app →](https://prosemirror-example.onrender.com/) • See the [example](./example) directory for a local setup.

## Installation

```bash
npm install @lix-js/sdk @lix-js/plugin-prosemirror
```

## Getting Started
## Quick start

### Initialize Lix with the Prosemirror Plugin
1) Open Lix with the ProseMirror plugin:

```ts
import { openLix } from "@lix-js/sdk";
import { plugin as prosemirrorPlugin } from "@lix-js/plugin-prosemirror";

export const lix = await openLix({
providePlugins: [prosemirrorPlugin],
});
export const lix = await openLix({ providePlugins: [prosemirrorPlugin] });
```

### Create and Insert a Prosemirror Document
2) Create a ProseMirror doc in Lix:

```ts
export const prosemirrorFile = await lix.db
const file = await lix.db
.insertInto("file")
.values({
path: "/prosemirror.json",
path: "/doc.json",
data: new TextEncoder().encode(
JSON.stringify({
type: "doc",
content: [],
}),
JSON.stringify({ type: "doc", content: [] }),
),
})
.execute();
.returningAll()
.executeTakeFirstOrThrow();
```

### Add the `lixProsemirror` Plugin to Your Editor State

When configuring your ProseMirror editor, add the `lixProsemirror` plugin to your editor state's plugins array:
3) Wire the editor:

```ts
import { lixProsemirror, idPlugin } from "@lix-js/plugin-prosemirror";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { lixProsemirror, idPlugin } from "@lix-js/plugin-prosemirror";

const state = EditorState.create({
doc: schema.nodeFromJSON(/* ... */),
schema,
doc: schema.nodeFromJSON({ type: "doc", content: [] }),
plugins: [
// ...other plugins...
idPlugin(),
lixProsemirror({
lix, // your lix instance
fileId: prosemirrorFile.id, // the file id of your Prosemirror document
}),
idPlugin(), // add stable ids if your schema doesn't provide them
lixProsemirror({ lix, fileId: file.id }),
],
});

const view = new EditorView(document.querySelector("#editor"), { state });
```

### (Optional) Add the `idPlugin` if Your Nodes Lack Unique IDs
## How it works

If your ProseMirror document nodes do not have unique IDs, you should also add the `idPlugin`:
- Node-per-entity: each node with an `attrs.id` becomes a stored entity. The document entity keeps the ordered list of child IDs.
- Content vs. structure: leaf nodes track text/marks; container nodes track attributes and the order of their children. New child nodes are captured inside their parent snapshot the first time they appear.
- Apply: `applyChanges` rebuilds the ProseMirror JSON by merging stored node snapshots and the document’s `children_order`.

```ts
import { idPlugin } from "@lix-js/plugin-prosemirror";
// ...other plugins...
idPlugin(),
```
## Requirements and tips

## How it works
- Every node you want tracked must have a stable, unique ID in `attrs.id`. Use `idPlugin()` if your schema does not supply IDs.
- Make sure your schema allows an `id` attribute on the node types you want to track.
- Reordering children is captured via the document’s `children_order`. Inserting in the middle or reparenting nodes will appear as moves plus any node-level edits.
- Text nodes themselves do not need IDs; their parent leaf node carries the content diff.

The lix prosemirror plugin tracks changes in the Prosemirror document with unique IDs.
### Adding IDs automatically

If you don't have a id for your nodes yet, use the `idPlugin()` to add them:
If your nodes don’t already have IDs, add the bundled `idPlugin()` so the ProseMirror plugin can track them:

```diff
{
Expand All @@ -101,3 +93,33 @@ If you don't have a id for your nodes yet, use the `idPlugin()` to add them:
]
}
```

## Rendering diffs

You can render review-friendly diffs with [HTML Diff](https://html-diff.lix.dev/). Serialize the before/after ProseMirror JSON to HTML using your schema, then feed the two HTML strings to HTML Diff:

```ts
import { renderHtmlDiff } from "@lix-js/html-diff";
import { DOMSerializer, Schema } from "prosemirror-model";

// Convert ProseMirror JSON to HTML
const toHtml = (schema: Schema, json: any) => {
const doc = schema.nodeFromJSON(json);
const div = document.createElement("div");
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
div.appendChild(fragment);
return div.innerHTML;
};

const beforeHtml = toHtml(schema, beforeJson);
const afterHtml = toHtml(schema, afterJson);

const diffHtml = await renderHtmlDiff({
beforeHtml,
afterHtml,
diffAttribute: "data-diff-key",
});

// Render it wherever you show reviews
document.querySelector("#diff")!.innerHTML = diffHtml;
```
29 changes: 29 additions & 0 deletions packages/lix/plugin-prosemirror/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,35 @@ function App() {
return (
<LixProvider lix={lix}>
<div className="flex flex-col mx-5 bg-base-100 text-base-content">
<a
href="https://www.npmjs.com/package/@lix-js/plugin-prosemirror"
target="_blank"
rel="noreferrer"
className="mt-4 flex items-center justify-between rounded border border-base-300 bg-base-100 px-4 py-3 shadow-sm transition hover:shadow-md"
>
<div className="flex items-center gap-3">
<div className="flex h-10 items-center justify-center rounded-md border border-base-300 bg-white px-3">
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/1080px-Npm-logo.svg.png"
alt="npm logo"
className="h-6 w-auto"
loading="lazy"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-sm text-neutral-600">
For more information visit the npm package.
</span>
<span className="text-lg font-semibold text-base-content">
@lix-js/plugin-prosemirror
</span>
</div>
</div>
<span className="inline-flex items-center justify-center rounded-md border border-base-300 bg-base-100 px-3 py-2 text-sm font-medium text-base-content hover:bg-base-200">
View package &rarr;
</span>
</a>

{/* main ui */}
<div className="flex flex-col border border-base-300 rounded my-5">
<div className="flex justify-between items-center mt-5 mb-5 mx-5">
Expand Down
Loading