diff --git a/examples/vite_basic/src/App.tsx b/examples/vite_basic/src/App.tsx
index 9296a65..4b460b7 100644
--- a/examples/vite_basic/src/App.tsx
+++ b/examples/vite_basic/src/App.tsx
@@ -18,6 +18,12 @@ const App = () => {
handleFileChange,
} = useFileLoader(AVAILABLE_FILES[0]);
+ console.log("test page ", testPage);
+
+ if (testPage) {
+ testPage.children = [undefined];
+ }
+
// Get backrefs for the currently selected file
const currentBackrefs = selectedFile?.backrefs || [];
diff --git a/typescript/src/renderer/JsonDocRenderer.tsx b/typescript/src/renderer/JsonDocRenderer.tsx
index c9da51b..e4c4238 100644
--- a/typescript/src/renderer/JsonDocRenderer.tsx
+++ b/typescript/src/renderer/JsonDocRenderer.tsx
@@ -1,15 +1,10 @@
import "./styles/index.css";
-import React, { useEffect } from "react";
+import React from "react";
import { Page } from "@/models/generated";
-import { loadPage } from "@/serialization/loader";
import { BlockRenderer } from "./components/BlockRenderer";
-import { PageDelimiter } from "./components/PageDelimiter";
-import { JsonViewPanel } from "./components/dev/JsonViewPanel";
-import { RendererProvider } from "./context/RendererContext";
-import { HighlightNavigation } from "./components/HighlightNavigation";
-import { useHighlights } from "./hooks/useHighlights";
+import { RendererContainer } from "./components/RendererContainer";
import { Backref } from "./utils/highlightUtils";
import { GlobalErrorBoundary } from "./components/ErrorBoundary";
@@ -40,101 +35,22 @@ export const JsonDocRenderer = ({
backrefs = [],
onError,
}: JsonDocRendererProps) => {
- // Use the modular hooks for highlight management
- const { highlightCount, currentActiveIndex, navigateToHighlight } =
- useHighlights({
- backrefs,
- });
-
- useEffect(() => {
- try {
- //TODO: this is not throwing for invalid page object (one that doesn't follow schema)
- loadPage(page);
- } catch (_) {
- // console.log("error ", error);
- }
- }, [page]);
-
- // return null;
- const renderedContent = (
-
- {/* Page icon */}
- {page.icon && (
-
- {page.icon.type === "emoji" && page.icon.emoji}
-
- )}
- {/* Page title */}
- {page.properties?.title && (
-
- {page.properties.title.title?.[0]?.plain_text || "Untitled"}
-
- )}
- {/* Page children blocks */}
- {page.children && page.children.length > 0 && (
-
- {page.children.map((block: any, index: number) => {
- const currentPageNum = block.metadata?.origin?.page_num;
- const nextPageNum =
- index < page.children.length - 1
- ? (page.children[index + 1]?.metadata as any)?.origin?.page_num
- : null;
-
- // Show delimiter after the last block of each page
- const showPageDelimiter =
- currentPageNum &&
- (nextPageNum !== currentPageNum ||
- index === page.children.length - 1);
-
- return (
-
-
-
- {showPageDelimiter && !components?.page_delimiter && (
-
- )}
- {showPageDelimiter && components?.page_delimiter && (
-
- )}
-
- );
- })}
-
- )}
-
- );
-
+ console.log("theme: ", theme);
return (
-
-
- {viewJson ? (
-
-
-
- ) : (
- renderedContent
- )}
- {/* Show highlight navigation when there are highlights */}
- {highlightCount > 0 && (
-
- )}
-
-
+
);
diff --git a/typescript/src/renderer/components/ErrorBoundary.tsx b/typescript/src/renderer/components/ErrorBoundary.tsx
index a980e32..8945dee 100644
--- a/typescript/src/renderer/components/ErrorBoundary.tsx
+++ b/typescript/src/renderer/components/ErrorBoundary.tsx
@@ -9,7 +9,7 @@ interface GlobalErrorBoundaryProps {
function GlobalErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
console.log("error ", error);
return (
-
+
Document Failed to Load
Something went wrong while rendering this document.
diff --git a/typescript/src/renderer/components/RendererContainer.tsx b/typescript/src/renderer/components/RendererContainer.tsx
new file mode 100644
index 0000000..cd5e654
--- /dev/null
+++ b/typescript/src/renderer/components/RendererContainer.tsx
@@ -0,0 +1,130 @@
+import React, { useEffect } from "react";
+
+import { Page } from "@/models/generated";
+import { loadPage } from "@/serialization/loader";
+
+import { RendererProvider } from "../context/RendererContext";
+import { useHighlights } from "../hooks/useHighlights";
+import { Backref } from "../utils/highlightUtils";
+
+import { HighlightNavigation } from "./HighlightNavigation";
+import { JsonViewPanel } from "./dev/JsonViewPanel";
+import { BlockRenderer } from "./BlockRenderer";
+import { PageDelimiter } from "./PageDelimiter";
+
+interface RendererContainerProps {
+ page: Page;
+ className?: string;
+ components?: React.ComponentProps
["components"] & {
+ page_delimiter: React.ComponentType<{
+ pageNumber: number;
+ }>;
+ };
+ devMode?: boolean;
+ resolveImageUrl?: (url: string) => Promise;
+ viewJson?: boolean;
+ backrefs?: Backref[];
+}
+
+export const RendererContainer: React.FC = ({
+ page,
+ className = "",
+ components,
+ devMode = false,
+ resolveImageUrl,
+ viewJson = false,
+ backrefs = [],
+}) => {
+ // Use the modular hooks for highlight management
+ const { highlightCount, currentActiveIndex, navigateToHighlight } =
+ useHighlights({
+ backrefs,
+ });
+
+ useEffect(() => {
+ try {
+ //TODO: this is not throwing for invalid page object (one that doesn't follow schema)
+ loadPage(page);
+ } catch (_) {
+ // console.log("error ", error);
+ }
+ }, [page]);
+
+ const renderedContent = (
+
+ {/* Page icon */}
+ {page.icon && (
+
+ {page.icon.type === "emoji" && page.icon.emoji}
+
+ )}
+ {/* Page title */}
+ {page.properties?.title && (
+
+ {page.properties.title.title?.[0]?.plain_text || "Untitled"}
+
+ )}
+ {/* Page children blocks */}
+ {page.children && page.children.length > 0 && (
+
+ {page.children.map((block: any, index: number) => {
+ const currentPageNum = block.metadata?.origin?.page_num;
+ const nextPageNum =
+ index < page.children.length - 1
+ ? (page.children[index + 1]?.metadata as any)?.origin?.page_num
+ : null;
+
+ // Show delimiter after the last block of each page
+ const showPageDelimiter =
+ currentPageNum &&
+ (nextPageNum !== currentPageNum ||
+ index === page.children.length - 1);
+
+ return (
+
+
+
+ {showPageDelimiter && !components?.page_delimiter && (
+
+ )}
+ {showPageDelimiter && components?.page_delimiter && (
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ );
+
+ return (
+
+
+ {viewJson ? (
+
+
+
+ ) : (
+ renderedContent
+ )}
+ {/* Show highlight navigation when there are highlights */}
+ {highlightCount > 0 && (
+
+ )}
+
+
+ );
+};
diff --git a/typescript/src/renderer/utils/highlightUtils.ts b/typescript/src/renderer/utils/highlightUtils.ts
index 57f3c17..3d9aed3 100644
--- a/typescript/src/renderer/utils/highlightUtils.ts
+++ b/typescript/src/renderer/utils/highlightUtils.ts
@@ -17,6 +17,7 @@ export function createHighlightSpan(text: string): HTMLSpanElement {
const highlightSpan = document.createElement("span");
highlightSpan.className = "json-doc-highlight";
highlightSpan.textContent = text;
+ highlightSpan.role = "note";
return highlightSpan;
}
diff --git a/typescript/tests/BlockRenderer.test.tsx b/typescript/tests/BlockRenderer.test.tsx
index 18e7419..b52ff56 100644
--- a/typescript/tests/BlockRenderer.test.tsx
+++ b/typescript/tests/BlockRenderer.test.tsx
@@ -6,27 +6,7 @@ import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { JsonDocRenderer } from "../src/renderer/JsonDocRenderer";
import { mockBlocks, mockPageWithAllBlocks } from "./fixtures/test-blocks";
-
-// Helper to create a page with specific blocks
-const createPageWithBlocks = (blocks: any[]) => ({
- object: "page",
- id: "test-page",
- properties: {
- title: {
- type: "title",
- title: [
- {
- href: null,
- type: "text",
- text: { link: null, content: "Test Page" },
- annotations: {},
- plain_text: "Test Page",
- },
- ],
- },
- },
- children: blocks,
-});
+import { createPageWithBlocks } from "./utils/helpers";
describe("JsonDocRenderer - All Block Types", () => {
it("renders page title correctly", () => {
@@ -50,7 +30,7 @@ describe("JsonDocRenderer - All Block Types", () => {
const page = createPageWithBlocks([mockBlocks.heading_1]);
render();
- screen.debug();
+ // screen.debug();
expect(screen.getByText("Main Heading")).toBeInTheDocument();
});
@@ -204,13 +184,10 @@ describe("JsonDocRenderer - All Block Types", () => {
).toBeInTheDocument();
});
- it("renders text annotations correctly", () => {
+ it("renders text annotations (bold, italic, underline etc.) correctly", () => {
const page = createPageWithBlocks([mockBlocks.paragraph]);
const { container } = render();
- // Debug: Print the HTML structure
- // screen.debug();
-
// Test bold annotation
const boldText = screen.getByText("bold text");
expect(boldText).toBeInTheDocument();
@@ -246,4 +223,79 @@ describe("JsonDocRenderer - All Block Types", () => {
expect(screen.getByText("Header 1")).toBeInTheDocument();
expect(screen.getByText("Content in first column")).toBeInTheDocument();
});
+
+ it("incorrect block page number shouldn't break the rendering", () => {
+ // Test misordered page numbers (3, 1, 2)
+ const blocksWithMisorderedPages = [
+ {
+ ...mockBlocks.paragraph,
+ metadata: {
+ origin: {
+ file_id: "test-file",
+ page_num: 3,
+ },
+ },
+ },
+ {
+ ...mockBlocks.heading_1,
+ metadata: {
+ origin: {
+ file_id: "test-file",
+ page_num: 1,
+ },
+ },
+ },
+ {
+ ...mockBlocks.heading_2,
+ metadata: {
+ origin: {
+ file_id: "test-file",
+ page_num: 2,
+ },
+ },
+ },
+ ];
+
+ const pageWithMisorderedBlocks = createPageWithBlocks(
+ blocksWithMisorderedPages
+ );
+
+ // This should not throw an error or break rendering
+ expect(() => {
+ render();
+ }).not.toThrow();
+
+ // Verify that content still renders despite misordered page numbers
+ expect(screen.getByText(/This is a paragraph with/)).toBeInTheDocument();
+ expect(screen.getByText(/Main Heading/)).toBeInTheDocument();
+ expect(screen.getByText(/Subheading/)).toBeInTheDocument();
+
+ // Verify page delimiters are rendered in the order they appear
+ expect(screen.getByText(/Page 3/)).toBeInTheDocument();
+ expect(screen.getByText(/Page 1/)).toBeInTheDocument();
+ expect(screen.getByText(/Page 2/)).toBeInTheDocument();
+ });
+
+ it("should handle bad page input gracefully", () => {
+ const badBlocks = [
+ undefined,
+ {
+ ...mockBlocks.paragraph,
+ metadata: {
+ origin: {
+ file_id: "test-file",
+ page_num: 3,
+ },
+ },
+ },
+ ];
+ const badPage = createPageWithBlocks(badBlocks);
+
+ render();
+
+ screen.debug();
+
+ expect(screen.getByRole("alert")).toBeInTheDocument();
+ expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
+ });
});
diff --git a/typescript/tests/Highlighting.test.tsx b/typescript/tests/Highlighting.test.tsx
new file mode 100644
index 0000000..474047f
--- /dev/null
+++ b/typescript/tests/Highlighting.test.tsx
@@ -0,0 +1,113 @@
+import "@testing-library/jest-dom";
+import { describe, it, expect, vi } from "vitest";
+import { render, screen, act, within } from "@testing-library/react";
+import React from "react";
+
+import { JsonDocRenderer } from "../src/renderer/JsonDocRenderer";
+import { mockPageWithAllBlocks } from "./fixtures/test-blocks";
+import { Backref } from "../src/renderer/utils/highlightUtils";
+
+describe("JsonDocRenderer Highlighting", () => {
+ it("creates highlights when backrefs are provided", async () => {
+ const paragraphBackref = {
+ start_idx: 4,
+ end_idx: 40,
+ block_id: "para-1",
+ };
+
+ const headingBackref = {
+ start_idx: 0,
+ end_idx: 4,
+ block_id: "h1-1",
+ };
+
+ const backrefs: Backref[] = [paragraphBackref, headingBackref];
+
+ const { container } = render(
+
+ );
+
+ // Wait for highlights to be processed
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ });
+
+ // // Check that highlight spans are created
+ const highlights = within(container).getAllByRole("note");
+ expect(highlights.length).toBeGreaterThan(0);
+
+ screen.debug();
+
+ const paragraphEl = container.querySelector(
+ `[data-block-id=${paragraphBackref.block_id}]`
+ );
+ expect(paragraphEl).toBeInTheDocument();
+
+ const headingEl = container.querySelector(
+ `[data-block-id=${headingBackref.block_id}]`
+ );
+ expect(headingEl).toBeInTheDocument();
+
+ const combinedElText =
+ (paragraphEl?.textContent || "") + " " + (headingEl?.textContent || "");
+
+ highlights.forEach((item) => {
+ expect(item.textContent?.length).toBeGreaterThan(0);
+ if (item.textContent)
+ expect(combinedElText.includes(item.textContent)).toBeTruthy();
+ });
+ });
+
+ it("highlights page title when page_id backref is provided", async () => {
+ const firstBackref = {
+ start_idx: 0,
+ end_idx: 4,
+ page_id: mockPageWithAllBlocks.id,
+ };
+ const backrefs: Backref[] = [firstBackref];
+
+ const { container } = render(
+
+ );
+
+ // Wait for highlights to be processed
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ });
+
+ // Check that highlight spans are created in the page title
+ const pageTitle = within(container).getAllByRole("heading")[0];
+ const highlightsInTitle = within(pageTitle).getAllByRole("note");
+
+ expect(highlightsInTitle.length).toBeGreaterThan(0);
+
+ const content =
+ mockPageWithAllBlocks.properties.title.title[0].text.content;
+ const textToHighlight = content.slice(
+ firstBackref.start_idx,
+ firstBackref.end_idx
+ );
+
+ expect(highlightsInTitle[0]).toHaveTextContent(textToHighlight);
+ });
+
+ it("handles invalid backrefs gracefully", async () => {
+ const backrefs: Backref[] = [
+ { start_idx: 0, end_idx: 4, block_id: "non-existent" },
+ { start_idx: 99, end_idx: 1, block_id: "para-1" },
+ ];
+
+ const { container } = render(
+
+ );
+
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ });
+
+ const highlights = within(container).queryAllByRole("note");
+
+ // No highlights should be created
+ expect(highlights.length).equals(0);
+ });
+});
diff --git a/typescript/tests/ThemeSwitching.test.tsx b/typescript/tests/ThemeSwitching.test.tsx
index f2f63da..b8035c4 100644
--- a/typescript/tests/ThemeSwitching.test.tsx
+++ b/typescript/tests/ThemeSwitching.test.tsx
@@ -5,32 +5,12 @@ import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { JsonDocRenderer } from "../src/renderer/JsonDocRenderer";
import { mockBlocks, mockPageWithAllBlocks } from "./fixtures/test-blocks";
-
-// Helper to create a simple page for theme testing
-const createSimplePage = () => ({
- object: "page",
- id: "theme-test-page",
- properties: {
- title: {
- type: "title",
- title: [
- {
- href: null,
- type: "text",
- text: { link: null, content: "Theme Test Page" },
- annotations: {},
- plain_text: "Theme Test Page",
- },
- ],
- },
- },
- children: [mockBlocks.paragraph],
-});
+import { createPageWithBlocks } from "./utils/helpers";
const JSON_DOC_ROOT_TEST_ID = "jsondoc-renderer-root";
describe("JsonDocRenderer - Theme Switching", () => {
- const simplePage = createSimplePage();
+ const simplePage = createPageWithBlocks([mockBlocks.paragraph]);
it("renders with light theme by default", () => {
const { container } = render();
diff --git a/typescript/tests/UnsupportedBlockHandling.test.tsx b/typescript/tests/UnsupportedBlockHandling.test.tsx
index 160a78d..b5fc126 100644
--- a/typescript/tests/UnsupportedBlockHandling.test.tsx
+++ b/typescript/tests/UnsupportedBlockHandling.test.tsx
@@ -3,27 +3,7 @@ import { render, screen } from "@testing-library/react";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { JsonDocRenderer } from "../src/renderer/JsonDocRenderer";
import { mockBlocks } from "./fixtures/test-blocks";
-
-// Helper to create a page with specific blocks
-const createPageWithBlocks = (blocks: any[]) => ({
- object: "page",
- id: "test-page",
- properties: {
- title: {
- type: "title",
- title: [
- {
- href: null,
- type: "text",
- text: { link: null, content: "Test Page" },
- annotations: {},
- plain_text: "Test Page",
- },
- ],
- },
- },
- children: blocks,
-});
+import { createPageWithBlocks } from "./utils/helpers";
describe("Unsupported Block Handling", () => {
// Mock console.warn to test logging behavior
diff --git a/typescript/tests/fixtures/test-blocks.ts b/typescript/tests/fixtures/test-blocks.ts
index 8293a72..46752ab 100644
--- a/typescript/tests/fixtures/test-blocks.ts
+++ b/typescript/tests/fixtures/test-blocks.ts
@@ -18,10 +18,7 @@ import {
Page,
} from "@/models/generated";
-// Mock data for all block types to test comprehensive rendering
-export const mockBlocks: Record<
- string,
- | Block
+type AllBlocks =
| ParagraphBlock
| Heading1Block
| Heading2Block
@@ -36,8 +33,12 @@ export const mockBlocks: Record<
| ToggleBlock
| TableBlock
| ColumnListBlock
- | EquationBlock
-> = {
+ | EquationBlock;
+
+type BlockType = Block["type"];
+
+// Mock data for all block types to test comprehensive rendering
+export const mockBlocks: Record = {
paragraph: {
object: "block",
id: "para-1",
@@ -428,6 +429,7 @@ export const mockBlocks: Record<
type: "paragraph",
created_time: "2025-06-24T09:44:12.014249Z",
has_children: false,
+ // @ts-expect-error
paragraph: {
rich_text: [
{
@@ -470,7 +472,6 @@ export const mockBlocks: Record<
type: "ai_block" as any,
created_time: "2025-06-24T09:44:12.014249Z",
has_children: false,
- // @ts-expect-error
ai_block: {
prompt: "Generate something amazing",
},
diff --git a/typescript/tests/setup.ts b/typescript/tests/setup.ts
index 6bf66a5..c46f0f4 100644
--- a/typescript/tests/setup.ts
+++ b/typescript/tests/setup.ts
@@ -11,3 +11,6 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
unobserve: vi.fn(),
takeRecords: vi.fn(() => []),
}));
+
+// Mock scrollIntoView for jsdom
+Element.prototype.scrollIntoView = vi.fn();
diff --git a/typescript/tests/test-cases.md b/typescript/tests/test-cases.md
index b980a0c..399cea8 100644
--- a/typescript/tests/test-cases.md
+++ b/typescript/tests/test-cases.md
@@ -1,8 +1,8 @@
-- page title highlighting work
-- block highlighting should work
-- invalid backref shouldn't trigger highlighting
-- incorrect block page number shouldn't break the rendering
-- make sure all blocks are rendered
-- make sure numbered lists are correctly labeled
-- make sure bad page is gracefully handled
-- invalid block type should show error block UI
+
+
+
+
+
+
+
+
diff --git a/typescript/tests/utils/helpers.ts b/typescript/tests/utils/helpers.ts
new file mode 100644
index 0000000..94ca5c4
--- /dev/null
+++ b/typescript/tests/utils/helpers.ts
@@ -0,0 +1,20 @@
+// Helper to create a page with specific blocks
+export const createPageWithBlocks = (blocks: any[]) => ({
+ object: "page",
+ id: "test-page",
+ properties: {
+ title: {
+ type: "title",
+ title: [
+ {
+ href: null,
+ type: "text",
+ text: { link: null, content: "Test Page" },
+ annotations: {},
+ plain_text: "Test Page",
+ },
+ ],
+ },
+ },
+ children: blocks,
+});