Skip to content

Commit

Permalink
Fix footnote reference insertions (#4)
Browse files Browse the repository at this point in the history
* fix footnote reference insertions

* add tests

* add jsdom dep

* update deletion test case

* update version - 2.0.1
  • Loading branch information
tk04 authored Jul 22, 2024
1 parent dc1dd32 commit cd29477
Show file tree
Hide file tree
Showing 7 changed files with 802 additions and 52 deletions.
12 changes: 9 additions & 3 deletions package/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "tiptap-footnotes",
"version": "2.0.0",
"version": "2.0.1",
"description": "A footnotes extension for Tiptap",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.mjs",
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"prepack": "pnpm run build"
"prepack": "pnpm run build",
"test": "vitest --environment=jsdom"
},
"exports": {
"import": "./dist/index.mjs",
Expand All @@ -27,13 +28,18 @@
"license": "MIT",
"devDependencies": {
"@tiptap/core": "^2.4.0",
"@tiptap/extension-document": "^2.4.0",
"@tiptap/extension-list-item": "^2.4.0",
"@tiptap/extension-ordered-list": "^2.4.0",
"@tiptap/extension-paragraph": "^2.5.5",
"@tiptap/extension-text": "^2.5.5",
"@tiptap/pm": "^2.4.0",
"@types/uuid": "^9.0.8",
"esbuild": "^0.21.4",
"jsdom": "^24.1.1",
"tsup": "^8.0.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"vitest": "^2.0.4"
},
"peerDependencies": {
"@tiptap/core": "^2.4.0",
Expand Down
6 changes: 4 additions & 2 deletions package/src/footnotes/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { mergeAttributes, Node } from "@tiptap/core";
import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";

import { v4 as uuid } from "uuid";
import { updateFootnoteReferences } from "./utils";

const REFNUM_ATTR = "data-reference-number";
const REF_CLASS = "footnote-ref";
Expand Down Expand Up @@ -116,7 +115,10 @@ const FootnoteReference = Node.create({
addFootnote:
() =>
({ state, tr }) => {
updateFootnoteReferences(tr, state, true);
const node = this.type.create({
"data-id": uuid(),
});
tr.insert(state.selection.anchor, node);
return true;
},
};
Expand Down
5 changes: 3 additions & 2 deletions package/src/footnotes/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const FootnoteRules = Extension.create({
if (refsChanged) break;

const isDelete = step.from != step.to; // the user deleted items from the document (from != to & the step is a replace step)
const isInsert = step.from === step.to && step.slice.size > 0;
const isInsert = step.slice.size > 0;

// check if any footnote references have been inserted
if (isInsert) {
Expand All @@ -62,7 +62,8 @@ const FootnoteRules = Extension.create({
return false;
}
});
} else if (isDelete) {
}
if (isDelete && !refsChanged) {
// check if any footnote references have been deleted
tr.before.nodesBetween(
step.from,
Expand Down
39 changes: 4 additions & 35 deletions package/src/footnotes/utils.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,20 @@
import { EditorState, Transaction } from "@tiptap/pm/state";
import { Fragment, Node } from "@tiptap/pm/model";

import { v4 as uuid } from "uuid";

function createFootnoteRefNode(state: EditorState, refNum: number) {
const type = state.schema.nodes.footnoteReference;

return type.create({
referenceNumber: `${refNum}`,
"data-id": uuid(),
});
}

// this function updates the reference number of all the footnote references in the document & if insertNewRef is set to true, it inserts a new referenece at the current cursor position
export function updateFootnoteReferences(
tr: Transaction,
state: EditorState,
insertNewRef = false,
) {
// update the reference number of all the footnote references in the document
export function updateFootnoteReferences(tr: Transaction) {
let count = 1;
let newNodeInserted = false;
const insertPos = state.selection.anchor;

const nodes: any[] = [];

tr.doc.descendants((node, pos) => {
if (node.type.name == "footnoteReference") {
if (insertNewRef && !newNodeInserted && pos >= insertPos) {
const newFootnote = createFootnoteRefNode(state, count);
tr.insert(tr.mapping.map(insertPos), newFootnote);

count += 1;
newNodeInserted = true;
nodes.push(newFootnote);
}
tr.setNodeAttribute(tr.mapping.map(pos), "referenceNumber", `${count}`);
tr.setNodeAttribute(pos, "referenceNumber", `${count}`);

nodes.push(node);
count += 1;
}
});
if (insertNewRef && !newNodeInserted) {
const newFootnote = createFootnoteRefNode(state, count);
tr.insert(tr.mapping.map(insertPos), newFootnote);
nodes.push(newFootnote);
}

// return the updated footnote references (in the order that they appear in the document)
return nodes;
}
Expand All @@ -67,7 +36,7 @@ function getFootnotes(tr: Transaction) {

// update the "footnotes" ordered list based on the footnote references in the document
export function updateFootnotesList(tr: Transaction, state: EditorState) {
const footnoteReferences = updateFootnoteReferences(tr, state, false);
const footnoteReferences = updateFootnoteReferences(tr);

const footnoteType = state.schema.nodes.footnote;
const footnotesType = state.schema.nodes.footnotes;
Expand Down
79 changes: 79 additions & 0 deletions package/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect, test } from "vitest";
import { Footnotes, getFootnotes, newEditor } from "./utils";

function testMatchingFootnotes(footnotes: Footnotes) {
expect(footnotes.refs.length).toEqual(footnotes.footnotes.length);

for (let i = 0; i < footnotes.refs.length; i++) {
const ref = footnotes.refs[i];
const footnote = footnotes.footnotes[i];
let refNumber = String(i + 1);

expect(ref.attrs!["data-id"]).toEqual(footnote.attrs!["data-id"]);
expect(ref.attrs!["referenceNumber"]).toEqual(refNumber);
expect(footnote.attrs!["id"]).toEqual(`fn:${refNumber}`);
}
}

test("basic footnote reference insertion", () => {
const editor = newEditor();
editor.commands.addFootnote();
const footnotes = getFootnotes(editor);
testMatchingFootnotes(footnotes);
});

test("footnote insertion between two refs ", () => {
const editor = newEditor();
editor.commands.addFootnote();
editor.commands.insertContent("-----------");
editor.commands.addFootnote();

// add footnote betwween refs 1 & 2
editor.commands.setTextSelection(10);
editor.commands.addFootnote();

const footnotes = getFootnotes(editor);

testMatchingFootnotes(footnotes);
});

test("footnote insertion between two refs (with chained command)", () => {
const editor = newEditor();
editor.commands.addFootnote();
editor.commands.insertContent("-----------");
editor.commands.addFootnote();

// add footnote betwween refs 1 & 2
editor.commands.setTextSelection(10);

editor.chain().deleteRange({ from: 2, to: 5 }).addFootnote().run();

const footnotes = getFootnotes(editor);

testMatchingFootnotes(footnotes);
});

test("test footnote reference deletion", () => {
const editor = newEditor();
editor.commands.addFootnote();
editor.commands.addFootnote();
editor.commands.addFootnote();

const prevFootnotes = getFootnotes(editor);

// delete the first footnote ref
editor.commands.deleteRange({ from: 1, to: 3 });

const footnotes = getFootnotes(editor);

expect(footnotes.refs.length).toEqual(2);

expect(footnotes.refs[0].attrs["data-id"]).toEqual(
prevFootnotes.refs[1].attrs["data-id"],
);
expect(footnotes.refs[1].attrs["data-id"]).toEqual(
prevFootnotes.refs[2].attrs["data-id"],
);

testMatchingFootnotes(footnotes);
});
47 changes: 47 additions & 0 deletions package/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Editor } from "@tiptap/core";
import { Node } from "@tiptap/pm/model";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import { Footnote, Footnotes, FootnoteReference } from "../src/index";

type Footnotes = {
refs: Node[];
footnotes: Node[];
};
export function getFootnotes(editor: Editor) {
const footnotes: Footnotes = { refs: [], footnotes: [] };

editor.state.doc.descendants((node) => {
if (node.type.name == "footnoteReference") {
footnotes.refs.push(node);
} else if (node.type.name == "footnote") {
footnotes.footnotes.push(node);
}
});
return footnotes;
}

const createEditorEl = () => {
const editorEl = document.createElement("div");

editorEl.classList.add("tiptap");
document.body.appendChild(editorEl);

return editorEl;
};
export function newEditor() {
return new Editor({
element: createEditorEl(),
extensions: [
Document.extend({
content: "block+ footnotes?",
}),
Text,
Paragraph,
Footnotes,
Footnote,
FootnoteReference,
],
});
}
Loading

0 comments on commit cd29477

Please sign in to comment.