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 ? (