tfrere HF Staff commited on
Commit
905f030
·
1 Parent(s): 3d8d74d

refactor: store pre-computed attrs in citation/bibliography nodes

Browse files

Citation:
- Add 'label' attribute to citation extension
- CitationView persists computed label back into node attrs
- Server-side renderHTML reads label attr for complete output

Bibliography:
- Add 'renderedHtml' attribute to bibliography extension
- BibliographyView persists formatted HTML into node attrs
- Server-side renderHTML produces complete bibliography section

Footnote:
- Server-side renderHTML shows tooltip + footnote marker

These pre-computed attributes allow generateHTML() to produce
complete HTML without React NodeViews.

Made-with: Cursor

backend/src/publisher/extensions.ts CHANGED
@@ -29,18 +29,23 @@ const CitationServer = Node.create({
29
  inline: true,
30
  atom: true,
31
  addAttributes() {
32
- return { key: { default: "" } };
 
 
 
33
  },
34
  parseHTML() {
35
  return [{ tag: 'span[data-type="citation"]' }];
36
  },
37
- renderHTML({ HTMLAttributes }) {
 
38
  return [
39
  "span",
40
  mergeAttributes(HTMLAttributes, {
41
  "data-type": "citation",
42
  class: "citation-node",
43
  }),
 
44
  ];
45
  },
46
  });
@@ -49,16 +54,26 @@ const BibliographyServer = Node.create({
49
  name: "bibliography",
50
  group: "block",
51
  atom: true,
 
 
 
 
 
52
  parseHTML() {
53
  return [{ tag: 'div[data-type="bibliography"]' }];
54
  },
55
- renderHTML({ HTMLAttributes }) {
 
 
 
 
56
  return [
57
  "div",
58
  mergeAttributes(HTMLAttributes, {
59
  "data-type": "bibliography",
60
  class: "bibliography-block",
61
  }),
 
62
  ];
63
  },
64
  });
@@ -97,18 +112,23 @@ const FootnoteServer = Node.create({
97
  inline: true,
98
  atom: true,
99
  addAttributes() {
100
- return { content: { default: "" } };
 
 
101
  },
102
  parseHTML() {
103
  return [{ tag: 'span[data-type="footnote"]' }];
104
  },
105
- renderHTML({ HTMLAttributes }) {
106
  return [
107
  "span",
108
  mergeAttributes(HTMLAttributes, {
109
  "data-type": "footnote",
110
  class: "footnote-node",
 
 
111
  }),
 
112
  ];
113
  },
114
  });
 
29
  inline: true,
30
  atom: true,
31
  addAttributes() {
32
+ return {
33
+ key: { default: "" },
34
+ label: { default: "" },
35
+ };
36
  },
37
  parseHTML() {
38
  return [{ tag: 'span[data-type="citation"]' }];
39
  },
40
+ renderHTML({ HTMLAttributes, node }) {
41
+ const label = node.attrs.label || `[${node.attrs.key}]`;
42
  return [
43
  "span",
44
  mergeAttributes(HTMLAttributes, {
45
  "data-type": "citation",
46
  class: "citation-node",
47
  }),
48
+ label,
49
  ];
50
  },
51
  });
 
54
  name: "bibliography",
55
  group: "block",
56
  atom: true,
57
+ addAttributes() {
58
+ return {
59
+ renderedHtml: { default: "" },
60
+ };
61
+ },
62
  parseHTML() {
63
  return [{ tag: 'div[data-type="bibliography"]' }];
64
  },
65
+ renderHTML({ HTMLAttributes, node }) {
66
+ const content = node.attrs.renderedHtml || "";
67
+ const inner = content
68
+ ? `<h2 class="bibliography-title">References</h2><div class="bibliography-content">${content}</div>`
69
+ : '<p class="bibliography-empty">No citations</p>';
70
  return [
71
  "div",
72
  mergeAttributes(HTMLAttributes, {
73
  "data-type": "bibliography",
74
  class: "bibliography-block",
75
  }),
76
+ inner,
77
  ];
78
  },
79
  });
 
112
  inline: true,
113
  atom: true,
114
  addAttributes() {
115
+ return {
116
+ content: { default: "" },
117
+ };
118
  },
119
  parseHTML() {
120
  return [{ tag: 'span[data-type="footnote"]' }];
121
  },
122
+ renderHTML({ HTMLAttributes, node }) {
123
  return [
124
  "span",
125
  mergeAttributes(HTMLAttributes, {
126
  "data-type": "footnote",
127
  class: "footnote-node",
128
+ title: node.attrs.content,
129
+ tabindex: "0",
130
  }),
131
+ ["sup", { class: "footnote-marker" }, "*"],
132
  ];
133
  },
134
  });
frontend/src/editor/BibliographyView.tsx CHANGED
@@ -2,7 +2,7 @@ import { NodeViewWrapper } from "@tiptap/react";
2
  import { useEffect, useState, useCallback, useRef } from "react";
3
  import type { NodeViewProps } from "@tiptap/react";
4
 
5
- export function BibliographyView({ editor }: NodeViewProps) {
6
  const [html, setHtml] = useState("");
7
  const [count, setCount] = useState(0);
8
  const [style, setStyle] = useState("apa");
@@ -62,7 +62,17 @@ export function BibliographyView({ editor }: NodeViewProps) {
62
  })
63
  .then((r) => r.json())
64
  .then((data) => {
65
- if (data.html) setHtml(data.html);
 
 
 
 
 
 
 
 
 
 
66
  })
67
  .catch(console.error);
68
  }, [editor, style]);
 
2
  import { useEffect, useState, useCallback, useRef } from "react";
3
  import type { NodeViewProps } from "@tiptap/react";
4
 
5
+ export function BibliographyView({ editor, node, getPos }: NodeViewProps) {
6
  const [html, setHtml] = useState("");
7
  const [count, setCount] = useState(0);
8
  const [style, setStyle] = useState("apa");
 
62
  })
63
  .then((r) => r.json())
64
  .then((data) => {
65
+ if (data.html) {
66
+ setHtml(data.html);
67
+ // Persist rendered HTML into node attrs for server-side generation
68
+ const pos = getPos();
69
+ if (typeof pos === "number" && node.attrs.renderedHtml !== data.html) {
70
+ editor.commands.command(({ tr }) => {
71
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, renderedHtml: data.html });
72
+ return true;
73
+ });
74
+ }
75
+ }
76
  })
77
  .catch(console.error);
78
  }, [editor, style]);
frontend/src/editor/CitationView.tsx CHANGED
@@ -45,18 +45,26 @@ export function CitationView({ node, editor, getPos }: NodeViewProps) {
45
 
46
  // Recompute label when entry or style changes
47
  useEffect(() => {
 
48
  if (!entry) {
49
- setLabel(`[${key}]`);
50
- return;
51
- }
52
-
53
- if (NUMERIC_STYLES.has(style)) {
54
  const order = getDocumentCitationOrder(editor);
55
  const idx = order.indexOf(key);
56
  const num = idx >= 0 ? idx + 1 : "?";
57
- setLabel(style === "ieee" ? `[${num}]` : `(${num})`);
58
  } else {
59
- setLabel(formatAuthorDate(entry));
 
 
 
 
 
 
 
 
 
 
60
  }
61
  }, [entry, style, key, editor]);
62
 
 
45
 
46
  // Recompute label when entry or style changes
47
  useEffect(() => {
48
+ let newLabel: string;
49
  if (!entry) {
50
+ newLabel = `[${key}]`;
51
+ } else if (NUMERIC_STYLES.has(style)) {
 
 
 
52
  const order = getDocumentCitationOrder(editor);
53
  const idx = order.indexOf(key);
54
  const num = idx >= 0 ? idx + 1 : "?";
55
+ newLabel = style === "ieee" ? `[${num}]` : `(${num})`;
56
  } else {
57
+ newLabel = formatAuthorDate(entry);
58
+ }
59
+ setLabel(newLabel);
60
+
61
+ // Persist label into node attrs for server-side HTML generation
62
+ const pos = getPos();
63
+ if (typeof pos === "number" && node.attrs.label !== newLabel) {
64
+ editor.commands.command(({ tr }) => {
65
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, label: newLabel });
66
+ return true;
67
+ });
68
  }
69
  }, [entry, style, key, editor]);
70
 
frontend/src/editor/extensions/bibliography.ts CHANGED
@@ -17,6 +17,12 @@ export const Bibliography = Node.create({
17
  draggable: false,
18
  selectable: true,
19
 
 
 
 
 
 
 
20
  parseHTML() {
21
  return [{ tag: 'div[data-type="bibliography"]' }];
22
  },
 
17
  draggable: false,
18
  selectable: true,
19
 
20
+ addAttributes() {
21
+ return {
22
+ renderedHtml: { default: "" },
23
+ };
24
+ },
25
+
26
  parseHTML() {
27
  return [{ tag: 'div[data-type="bibliography"]' }];
28
  },
frontend/src/editor/extensions/citation.ts CHANGED
@@ -26,6 +26,7 @@ export const Citation = Node.create({
26
  addAttributes() {
27
  return {
28
  key: { default: "" },
 
29
  };
30
  },
31
 
 
26
  addAttributes() {
27
  return {
28
  key: { default: "" },
29
+ label: { default: "" },
30
  };
31
  },
32