From dc9058872b31ce8108246213428fafda764e99d1 Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Fri, 20 Jun 2025 10:53:52 +0300 Subject: [PATCH 1/3] make page title highlighting work --- typescript/src/renderer/utils/blockMapping.js | 30 ------- .../src/renderer/utils/highlightUtils.ts | 39 ++++++++- .../src/renderer/utils/richTextRenderer.js | 86 ------------------- 3 files changed, 38 insertions(+), 117 deletions(-) delete mode 100644 typescript/src/renderer/utils/blockMapping.js delete mode 100644 typescript/src/renderer/utils/richTextRenderer.js diff --git a/typescript/src/renderer/utils/blockMapping.js b/typescript/src/renderer/utils/blockMapping.js deleted file mode 100644 index e6099db..0000000 --- a/typescript/src/renderer/utils/blockMapping.js +++ /dev/null @@ -1,30 +0,0 @@ -// Block type to component mapping -export const blockTypeMap = { - paragraph: "ParagraphBlock", - heading_1: "Heading1Block", - heading_2: "Heading2Block", - heading_3: "Heading3Block", - bulleted_list_item: "BulletedListBlock", - numbered_list_item: "NumberedListBlock", - to_do: "TodoBlock", - code: "CodeBlock", - quote: "QuoteBlock", - divider: "DividerBlock", - image: "ImageBlock", - equation: "EquationBlock", - table: "TableBlock", - table_row: "TableRowBlock", - column_list: "ColumnListBlock", - column: "ColumnBlock", - toggle: "ToggleBlock", -}; - -// Get component name for block type -export function getComponentForBlockType(blockType) { - return blockTypeMap[blockType] || "UnsupportedBlock"; -} - -// Get all supported block types -export function getSupportedBlockTypes() { - return Object.keys(blockTypeMap); -} diff --git a/typescript/src/renderer/utils/highlightUtils.ts b/typescript/src/renderer/utils/highlightUtils.ts index 212aa79..d1233bf 100644 --- a/typescript/src/renderer/utils/highlightUtils.ts +++ b/typescript/src/renderer/utils/highlightUtils.ts @@ -2,6 +2,7 @@ export interface Backref { end_idx: number; start_idx: number; block_id?: string; + page_id?: string; } export interface HighlightElement { @@ -88,9 +89,15 @@ export function highlightTextNode( } /** - * Processes a single backref and applies highlighting to the target block + * Processes a single backref and applies highlighting to the target block or page title */ export function processBackref(backref: Backref): HTMLSpanElement | null { + // If page_id is provided, target the page title + if (backref.page_id) { + return processPageTitleBackref(backref); + } + + // Otherwise, handle block highlighting as before if (!backref.block_id) { return null; } @@ -120,6 +127,36 @@ export function processBackref(backref: Backref): HTMLSpanElement | null { return firstSpanForThisBackref; } +/** + * Processes a backref that targets a page title + */ +export function processPageTitleBackref(backref: Backref): HTMLSpanElement | null { + // Find the specific page title element using data-page-id + const pageTitleElement = document.querySelector( + `[data-page-id="${backref.page_id}"]` + ); + + if (!pageTitleElement) { + return null; + } + + const textNodes = getTextNodes(pageTitleElement); + let currentIndex = 0; + let firstSpanForThisBackref: HTMLSpanElement | null = null; + + for (const textNode of textNodes) { + const highlightSpan = highlightTextNode(textNode, backref, currentIndex); + + if (highlightSpan && !firstSpanForThisBackref) { + firstSpanForThisBackref = highlightSpan; + } + + currentIndex += textNode.textContent?.length || 0; + } + + return firstSpanForThisBackref; +} + /** * Sorts highlight elements by their DOM position (reading order) */ diff --git a/typescript/src/renderer/utils/richTextRenderer.js b/typescript/src/renderer/utils/richTextRenderer.js deleted file mode 100644 index f413348..0000000 --- a/typescript/src/renderer/utils/richTextRenderer.js +++ /dev/null @@ -1,86 +0,0 @@ -// Rich Text Renderer utility -export function renderRichText(richText, createElement) { - if (!richText || richText.length === 0) { - return null; - } - - return richText.map((item, index) => { - const key = `rich-text-${index}`; - - if (item?.type === "text") { - const { text, annotations, href } = item; - const content = text?.content || ""; - - if (!content) return null; - - let element = createElement("span", { key }, content); - - // Apply text formatting - if (annotations) { - if (annotations.bold) { - element = createElement("strong", { key }, element); - } - if (annotations.italic) { - element = createElement("em", { key }, element); - } - if (annotations.strikethrough) { - element = createElement("del", { key }, element); - } - if (annotations.underline) { - element = createElement("u", { key }, element); - } - if (annotations.code) { - element = createElement( - "code", - { key, className: "notion-inline-code" }, - content - ); - } - if (annotations.color && annotations.color !== "default") { - element = createElement( - "span", - { - key, - className: `notion-text-color-${annotations.color}`, - }, - element - ); - } - } - - // Handle links - if (href) { - element = createElement( - "a", - { - key, - href, - className: "notion-link", - target: "_blank", - rel: "noopener noreferrer", - }, - element - ); - } - - return element; - } - - if (item?.type === "equation") { - return createElement("span", { - key, - className: "notion-equation", - dangerouslySetInnerHTML: { - __html: window.katex - ? window.katex.renderToString(item.equation?.expression || "", { - throwOnError: false, - displayMode: false, - }) - : item.equation?.expression || "", - }, - }); - } - - return null; - }); -} From 2e985ffd086d27805791756d1a73977acfe42279 Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Fri, 20 Jun 2025 14:36:29 +0300 Subject: [PATCH 2/3] error boundry --- typescript/README.md | 42 +++++++++- typescript/examples/vite_basic/src/App.tsx | 9 ++- typescript/package-lock.json | 20 +++++ typescript/package.json | 1 + typescript/src/renderer/JsonDocRenderer.tsx | 45 ++++++----- .../src/renderer/components/ErrorBoundary.tsx | 53 +++++++++++++ .../src/renderer/styles/error-boundry.css | 79 +++++++++++++++++++ typescript/src/renderer/styles/index.css | 4 +- typescript/src/validation/page-validator.ts | 30 +++++++ 9 files changed, 260 insertions(+), 23 deletions(-) create mode 100644 typescript/src/renderer/components/ErrorBoundary.tsx create mode 100644 typescript/src/renderer/styles/error-boundry.css create mode 100644 typescript/src/validation/page-validator.ts diff --git a/typescript/README.md b/typescript/README.md index 70c078f..98d3b49 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -307,4 +307,44 @@ You can choose a different version from the list or create new one. But bumpp is - [x] move vite app to typescript root examples dir - [ ] setup monorepo tooling - [ ] fix model generation for Image and RichText, then type renderers -- [ ] use katex or similar package for equations +- [x] use katex or similar package for equations +- [x] add error boundry +- [x] add page title highlighting +- [ ] validate page prop somehow. not clear how to do yet. we can't use the /schema because it's HUGE and also because it's outside of /typescript. Will need to think about this. + +maybe do something like this? +``` +import type { Page } from "../models/generated/page/page"; +import { isPage } from "../models/generated/essential-types"; + +export function validatePage(obj: unknown): obj is Page { + console.log("isPage(obj) ", isPage(obj)); + return ( + isPage(obj) && + typeof (obj as any).id === "string" && + Array.isArray((obj as any).children) + ); +} + +export function validatePageWithError(obj: unknown): { + valid: boolean; + error?: string; +} { + if (!isPage(obj)) { + return { valid: false, error: "Not a valid page object" }; + } + + if (typeof (obj as any).id !== "string") { + return { valid: false, error: "Page id must be a string" }; + } + + if (!Array.isArray((obj as any).children)) { + return { valid: false, error: "Page children must be an array" }; + } + + return { valid: true }; +} + +``` + +will require us to write a validator and we won't benefit from the defined schema jsons. \ No newline at end of file diff --git a/typescript/examples/vite_basic/src/App.tsx b/typescript/examples/vite_basic/src/App.tsx index 9c994d6..6fe57cf 100644 --- a/typescript/examples/vite_basic/src/App.tsx +++ b/typescript/examples/vite_basic/src/App.tsx @@ -6,9 +6,16 @@ import DevPanel from "./components/DevPanel"; // Test backrefs for highlighting const testBackrefs: Array<{ end_idx: number; - block_id: string; start_idx: number; + block_id?: string; + page_id?: string; }> = [ + // Test page title highlighting + { + end_idx: 15, + page_id: "pg_01jxm798ddfdvt60gy8nqh0xm7", + start_idx: 0, + }, // { // end_idx: 50, // block_id: "blk_table_row_5", diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 04b271b..7103a5e 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -13,6 +13,7 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "katex": "^0.16.22", + "react-error-boundary": "^6.0.0", "react-intersection-observer": "^9.13.0" }, "devDependencies": { @@ -540,6 +541,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -9424,6 +9433,17 @@ "react": "^18.3.1" } }, + "node_modules/react-error-boundary": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", + "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-intersection-observer": { "version": "9.16.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", diff --git a/typescript/package.json b/typescript/package.json index f156384..6c9e904 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -70,6 +70,7 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "katex": "^0.16.22", + "react-error-boundary": "^6.0.0", "react-intersection-observer": "^9.13.0" }, "peerDependencies": { diff --git a/typescript/src/renderer/JsonDocRenderer.tsx b/typescript/src/renderer/JsonDocRenderer.tsx index e207a07..6a8ed88 100644 --- a/typescript/src/renderer/JsonDocRenderer.tsx +++ b/typescript/src/renderer/JsonDocRenderer.tsx @@ -11,6 +11,7 @@ import { RendererProvider } from "./context/RendererContext"; import { HighlightNavigation } from "./components/HighlightNavigation"; import { useHighlights } from "./hooks/useHighlights"; import { Backref } from "./utils/highlightUtils"; +import { GlobalErrorBoundary } from "./components/ErrorBoundary"; interface JsonDocRendererProps { page: Page; @@ -25,6 +26,7 @@ interface JsonDocRendererProps { devMode?: boolean; viewJson?: boolean; backrefs?: Backref[]; + onError?: (error: Error, errorInfo: React.ErrorInfo) => void; } export const JsonDocRenderer = ({ @@ -36,6 +38,7 @@ export const JsonDocRenderer = ({ devMode = false, viewJson = false, backrefs = [], + onError, }: JsonDocRendererProps) => { // Use the modular hooks for highlight management const { highlightCount, currentActiveIndex, navigateToHighlight } = @@ -97,25 +100,29 @@ export const JsonDocRenderer = ({ ); return ( - -
- {viewJson ? ( -
-
{renderedContent}
- +
+ + +
+ {viewJson ? ( +
+
{renderedContent}
+ +
+ ) : ( + renderedContent + )} + {/* Show highlight navigation when there are highlights */} + {highlightCount > 0 && ( + + )}
- ) : ( - 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 new file mode 100644 index 0000000..a980e32 --- /dev/null +++ b/typescript/src/renderer/components/ErrorBoundary.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { ErrorBoundary, FallbackProps } from "react-error-boundary"; + +interface GlobalErrorBoundaryProps { + children: React.ReactNode; + onError?: (error: Error, errorInfo: React.ErrorInfo) => void; +} + +function GlobalErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + console.log("error ", error); + return ( +
+
+

Document Failed to Load

+

Something went wrong while rendering this document.

+
+          Message: {error.message}
+          {error.stack && (
+            
+ Stack Trace: + {error.stack} +
+ )} +
+ + +
+
+ ); +} + +export function GlobalErrorBoundary({ + children, + onError, +}: GlobalErrorBoundaryProps) { + return ( + { + // Optional: Add any cleanup logic here + window.location.reload(); + }} + > + {children} + + ); +} diff --git a/typescript/src/renderer/styles/error-boundry.css b/typescript/src/renderer/styles/error-boundry.css new file mode 100644 index 0000000..95ed6b8 --- /dev/null +++ b/typescript/src/renderer/styles/error-boundry.css @@ -0,0 +1,79 @@ +/* Error Boundary Styles */ +.json-doc-error-boundary { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + width: 100%; + /* max-width: 600px; */ + padding: var(--jsondoc-spacing-xl); + background: var(--jsondoc-bg-primary); + border: 1px solid var(--jsondoc-border-light); + border-radius: var(--jsondoc-radius-md); + margin: var(--jsondoc-spacing-lg); +} + +.json-doc-error-content { + text-align: center; + max-width: 500px; +} + +.json-doc-error-content h2 { + color: var(--jsondoc-text-primary); + font-size: var(--jsondoc-font-size-xl); + font-weight: var(--jsondoc-font-weight-bold); + margin: 0 0 var(--jsondoc-spacing-md) 0; +} + +.json-doc-error-content p { + color: var(--jsondoc-text-secondary); + font-size: var(--jsondoc-font-size-base); + margin: 0 0 var(--jsondoc-spacing-lg) 0; + line-height: var(--jsondoc-line-height-normal); +} + +.json-doc-error-details { + margin: var(--jsondoc-spacing-lg) 0; + text-align: left; +} + +.json-doc-error-details summary { + color: var(--jsondoc-text-secondary); + font-size: var(--jsondoc-font-size-sm); + cursor: pointer; + margin-bottom: var(--jsondoc-spacing-sm); +} + +.json-doc-error-boundary pre { + background: var(--jsondoc-bg-code); + color: var(--jsondoc-text-primary); + padding: var(--jsondoc-spacing-md); + border-radius: var(--jsondoc-radius-sm); + font-family: var(--jsondoc-font-family-mono); + font-size: var(--jsondoc-font-size-sm); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; +} + +.json-doc-error-retry-button { + background: var(--jsondoc-color-primary); + color: var(--jsondoc-text-primary); + border: none; + padding: var(--jsondoc-spacing-sm) var(--jsondoc-spacing-lg); + border-radius: var(--jsondoc-radius-sm); + font-size: var(--jsondoc-font-size-base); + font-weight: var(--jsondoc-font-weight-medium); + cursor: pointer; + transition: all var(--jsondoc-transition-fast); +} + +.json-doc-error-retry-button:hover { + background: var(--jsondoc-color-primary-hover); + transform: translateY(-1px); +} + +.json-doc-error-retry-button:active { + transform: translateY(0); +} diff --git a/typescript/src/renderer/styles/index.css b/typescript/src/renderer/styles/index.css index 8772d4e..13a4778 100644 --- a/typescript/src/renderer/styles/index.css +++ b/typescript/src/renderer/styles/index.css @@ -1,7 +1,7 @@ /* JSON-DOC Renderer Styles */ /* Import KaTeX styles for equations */ -@import "katex/dist/katex.min.css"; +/* @import "katex/dist/katex.min.css"; */ /* Import design tokens first */ @import "./variables.css"; @@ -16,6 +16,6 @@ @import "./table.css"; @import "./media.css"; @import "./layout.css"; - +@import "./error-boundry.css"; /* Import responsive styles last */ @import "./responsive.css"; diff --git a/typescript/src/validation/page-validator.ts b/typescript/src/validation/page-validator.ts new file mode 100644 index 0000000..743b27e --- /dev/null +++ b/typescript/src/validation/page-validator.ts @@ -0,0 +1,30 @@ +import type { Page } from "../models/generated/page/page"; +import { isPage } from "../models/generated/essential-types"; + +export function validatePage(obj: unknown): obj is Page { + console.log("isPage(obj) ", isPage(obj)); + return ( + isPage(obj) && + typeof (obj as any).id === "string" && + Array.isArray((obj as any).children) + ); +} + +export function validatePageWithError(obj: unknown): { + valid: boolean; + error?: string; +} { + if (!isPage(obj)) { + return { valid: false, error: "Not a valid page object" }; + } + + if (typeof (obj as any).id !== "string") { + return { valid: false, error: "Page id must be a string" }; + } + + if (!Array.isArray((obj as any).children)) { + return { valid: false, error: "Page children must be an array" }; + } + + return { valid: true }; +} From dc1f48cbedaa4a57e9a5be20208050c68d1c7296 Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Fri, 20 Jun 2025 14:56:40 +0300 Subject: [PATCH 3/3] fix test import --- typescript/tests/serialization.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/tests/serialization.test.ts b/typescript/tests/serialization.test.ts index ae7c0e9..b492d8f 100644 --- a/typescript/tests/serialization.test.ts +++ b/typescript/tests/serialization.test.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import * as JSON5 from "json5"; -import { loadJson, deepClone } from "../src"; +import { loadJson, deepClone } from "../src/utils/json"; // Path to the example page JSON file const PAGE_PATH = path.resolve(__dirname, "../../schema/page/ex1_success.json");