fix: bibliography not appearing - extract HTML before generateHTML
Browse filesThe renderedhtml attribute can contain > chars (valid in quoted HTML
attributes) which broke the [^>]* regex in postProcess. Now we extract
the bibliography HTML from the TipTap JSON tree before calling
generateHTML, clear the attribute to keep the output clean, and inject
the raw HTML in postProcess using the pre-extracted string.
Made-with: Cursor
backend/src/publisher/html-renderer.ts
CHANGED
|
@@ -32,8 +32,12 @@ export function renderArticleHTML(
|
|
| 32 |
cssArticle: string
|
| 33 |
): string {
|
| 34 |
const extensions = getServerExtensions();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
const bodyHtml = generateHTML(json as any, extensions);
|
| 36 |
-
const enrichedBody = postProcess(bodyHtml);
|
| 37 |
|
| 38 |
const authorNames = meta.authors.map((a) => a.name);
|
| 39 |
const authorsStr = authorNames.join(", ");
|
|
@@ -262,13 +266,30 @@ dialog.lightbox img {
|
|
| 262 |
</html>`;
|
| 263 |
}
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
/**
|
| 266 |
* Post-process the raw generateHTML output:
|
|
|
|
| 267 |
* - Transform accordion divs into <details><summary>
|
| 268 |
* - Transform htmlEmbed into iframes
|
| 269 |
-
* - Add semantic structure where possible
|
| 270 |
*/
|
| 271 |
-
function postProcess(html: string): string {
|
| 272 |
let result = html;
|
| 273 |
|
| 274 |
// Accordion: div[data-component="accordion"] → <details><summary>
|
|
@@ -285,23 +306,17 @@ function postProcess(html: string): string {
|
|
| 285 |
}
|
| 286 |
);
|
| 287 |
|
| 288 |
-
// Bibliography:
|
|
|
|
|
|
|
| 289 |
result = result.replace(
|
| 290 |
/<div([^>]*data-type="bibliography"[^>]*)><\/div>/gi,
|
| 291 |
(_match, attrs: string) => {
|
| 292 |
-
const htmlMatch = attrs.match(/renderedhtml="([^"]*)"/i);
|
| 293 |
const cleanAttrs = attrs.replace(/\s*renderedhtml="[^"]*"/i, "");
|
| 294 |
-
if (
|
| 295 |
-
return `<div${cleanAttrs}><
|
| 296 |
}
|
| 297 |
-
|
| 298 |
-
.replace(/&/g, "&")
|
| 299 |
-
.replace(/</g, "<")
|
| 300 |
-
.replace(/>/g, ">")
|
| 301 |
-
.replace(/"/g, '"')
|
| 302 |
-
.replace(/'/g, "'")
|
| 303 |
-
.replace(/'/g, "'");
|
| 304 |
-
return `<div${cleanAttrs}><h2 class="bibliography-title">References</h2><div class="bibliography-content">${decoded}</div></div>`;
|
| 305 |
}
|
| 306 |
);
|
| 307 |
|
|
|
|
| 32 |
cssArticle: string
|
| 33 |
): string {
|
| 34 |
const extensions = getServerExtensions();
|
| 35 |
+
|
| 36 |
+
// Extract bibliography HTML before generateHTML escapes it as an attribute
|
| 37 |
+
const biblioHtml = extractBibliographyHtml(json);
|
| 38 |
+
|
| 39 |
const bodyHtml = generateHTML(json as any, extensions);
|
| 40 |
+
const enrichedBody = postProcess(bodyHtml, biblioHtml);
|
| 41 |
|
| 42 |
const authorNames = meta.authors.map((a) => a.name);
|
| 43 |
const authorsStr = authorNames.join(", ");
|
|
|
|
| 266 |
</html>`;
|
| 267 |
}
|
| 268 |
|
| 269 |
+
/**
|
| 270 |
+
* Walk the TipTap JSON tree to find a bibliography node, extract its
|
| 271 |
+
* renderedHtml attribute, and clear it so generateHTML outputs a clean div.
|
| 272 |
+
*/
|
| 273 |
+
function extractBibliographyHtml(json: Record<string, unknown>): string {
|
| 274 |
+
const content = (json as any)?.content;
|
| 275 |
+
if (!Array.isArray(content)) return "";
|
| 276 |
+
for (const node of content) {
|
| 277 |
+
if (node.type === "bibliography") {
|
| 278 |
+
const html = node.attrs?.renderedHtml || "";
|
| 279 |
+
if (node.attrs) node.attrs.renderedHtml = "";
|
| 280 |
+
return html;
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
return "";
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
/**
|
| 287 |
* Post-process the raw generateHTML output:
|
| 288 |
+
* - Inject bibliography HTML (extracted before generateHTML)
|
| 289 |
* - Transform accordion divs into <details><summary>
|
| 290 |
* - Transform htmlEmbed into iframes
|
|
|
|
| 291 |
*/
|
| 292 |
+
function postProcess(html: string, biblioHtml: string): string {
|
| 293 |
let result = html;
|
| 294 |
|
| 295 |
// Accordion: div[data-component="accordion"] → <details><summary>
|
|
|
|
| 306 |
}
|
| 307 |
);
|
| 308 |
|
| 309 |
+
// Bibliography: replace empty placeholder with the pre-extracted HTML.
|
| 310 |
+
// renderedHtml was cleared before generateHTML, so the attribute is now
|
| 311 |
+
// empty and the div is clean (no > chars breaking the regex).
|
| 312 |
result = result.replace(
|
| 313 |
/<div([^>]*data-type="bibliography"[^>]*)><\/div>/gi,
|
| 314 |
(_match, attrs: string) => {
|
|
|
|
| 315 |
const cleanAttrs = attrs.replace(/\s*renderedhtml="[^"]*"/i, "");
|
| 316 |
+
if (biblioHtml) {
|
| 317 |
+
return `<div${cleanAttrs}><h2 class="bibliography-title">References</h2><div class="bibliography-content">${biblioHtml}</div></div>`;
|
| 318 |
}
|
| 319 |
+
return `<div${cleanAttrs}><p class="bibliography-empty">No citations</p></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
}
|
| 321 |
);
|
| 322 |
|