diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts
index 2dd62d89..fe2988dd 100644
--- a/actions/publishToPublication.ts
+++ b/actions/publishToPublication.ts
@@ -485,6 +485,17 @@ async function processBlocksToPages(
).flat();
}
async function blockToRecord(b: Block, did: string) {
+ const footnoteContentResolver = (footnoteEntityID: string) => {
+ let [content] = scan.eav(footnoteEntityID, "block/text");
+ if (!content) return { plaintext: "", facets: [] as PubLeafletRichtextFacet.Main[] };
+ let doc = new Y.Doc();
+ const update = base64.toByteArray(content.data.value);
+ Y.applyUpdate(doc, update);
+ let nodes = doc.getXmlElement("prosemirror").toArray();
+ let plaintext = YJSFragmentToString(nodes[0]);
+ let { facets } = YJSFragmentToFacets(nodes[0]);
+ return { plaintext, facets };
+ };
const getBlockContent = (b: string) => {
let [content] = scan.eav(b, "block/text");
if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const;
@@ -493,7 +504,7 @@ async function processBlocksToPages(
Y.applyUpdate(doc, update);
let nodes = doc.getXmlElement("prosemirror").toArray();
let stringValue = YJSFragmentToString(nodes[0]);
- let { facets } = YJSFragmentToFacets(nodes[0]);
+ let { facets } = YJSFragmentToFacets(nodes[0], 0, footnoteContentResolver);
return [stringValue, facets] as const;
};
if (b.type === "card") {
@@ -759,8 +770,34 @@ async function processBlocksToPages(
function YJSFragmentToFacets(
node: Y.XmlElement | Y.XmlText | Y.XmlHook,
byteOffset: number = 0,
+ footnoteContentResolver?: (footnoteEntityID: string) => { plaintext: string; facets: PubLeafletRichtextFacet.Main[] },
): { facets: PubLeafletRichtextFacet.Main[]; byteLength: number } {
if (node.constructor === Y.XmlElement) {
+ // Handle footnote inline nodes
+ if (node.nodeName === "footnote") {
+ const footnoteEntityID = node.getAttribute("footnoteEntityID") || "";
+ const placeholder = "*";
+ const unicodestring = new UnicodeString(placeholder);
+ let footnoteContent = footnoteContentResolver?.(footnoteEntityID);
+ const facet: PubLeafletRichtextFacet.Main = {
+ index: {
+ byteStart: byteOffset,
+ byteEnd: byteOffset + unicodestring.length,
+ },
+ features: [
+ {
+ $type: "pub.leaflet.richtext.facet#footnote",
+ footnoteId: footnoteEntityID,
+ contentPlaintext: footnoteContent?.plaintext || "",
+ ...(footnoteContent?.facets?.length
+ ? { contentFacets: footnoteContent.facets }
+ : {}),
+ },
+ ],
+ };
+ return { facets: [facet], byteLength: unicodestring.length };
+ }
+
// Handle inline mention nodes
if (node.nodeName === "didMention") {
const text = node.getAttribute("text") || "";
@@ -807,7 +844,7 @@ function YJSFragmentToFacets(
let allFacets: PubLeafletRichtextFacet.Main[] = [];
let currentOffset = byteOffset;
for (const child of node.toArray()) {
- const result = YJSFragmentToFacets(child, currentOffset);
+ const result = YJSFragmentToFacets(child, currentOffset, footnoteContentResolver);
allFacets.push(...result.facets);
currentOffset += result.byteLength;
}
diff --git a/app/[leaflet_id]/Footer.tsx b/app/[leaflet_id]/Footer.tsx
index ad063f02..f8ff45fa 100644
--- a/app/[leaflet_id]/Footer.tsx
+++ b/app/[leaflet_id]/Footer.tsx
@@ -4,6 +4,7 @@ import { Footer } from "components/ActionBar/Footer";
import { Media } from "components/Media";
import { ThemePopover } from "components/ThemeManager/ThemeSetter";
import { Toolbar } from "components/Toolbar";
+import { FootnoteToolbar } from "components/Toolbar/FootnoteToolbarWrapper";
import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions";
import { HomeButton } from "app/[leaflet_id]/actions/HomeButton";
import { PublishButton } from "./actions/PublishButton";
@@ -56,6 +57,17 @@ export function LeafletFooter(props: { entityID: string }) {
blockType={blockType}
/>
+ ) : focusedBlock &&
+ focusedBlock.entityType === "footnote" &&
+ entity_set.permissions.write ? (
+
{
+ if (e.currentTarget === e.target) e.preventDefault();
+ }}
+ >
+
+
) : entity_set.permissions.write ? (