fix: bound navigation and inspector rendering
This commit is contained in:
@@ -1,8 +1,16 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { ParserDiagnostic } from '../onenote/model/diagnostics.js';
|
import type { ParserDiagnostic } from '../onenote/model/diagnostics.js';
|
||||||
|
import {
|
||||||
|
DEFAULT_UI_RENDER_LIMITS,
|
||||||
|
limitUiString,
|
||||||
|
resolveUiLimit,
|
||||||
|
} from './ui-render-budget.js';
|
||||||
|
|
||||||
interface DiagnosticsPanelProps {
|
interface DiagnosticsPanelProps {
|
||||||
diagnostics: ParserDiagnostic[];
|
diagnostics: ParserDiagnostic[];
|
||||||
title?: string;
|
title?: string;
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOffset(offset: number): string {
|
function formatOffset(offset: number): string {
|
||||||
@@ -12,13 +20,69 @@ function formatOffset(offset: number): string {
|
|||||||
export function DiagnosticsPanel({
|
export function DiagnosticsPanel({
|
||||||
diagnostics,
|
diagnostics,
|
||||||
title = 'Diagnostics',
|
title = 'Diagnostics',
|
||||||
|
pageSize: requestedPageSize,
|
||||||
}: DiagnosticsPanelProps) {
|
}: DiagnosticsPanelProps) {
|
||||||
|
const [requestedPage, setRequestedPage] = useState(0);
|
||||||
|
const pageSize = resolveUiLimit(
|
||||||
|
requestedPageSize,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxDiagnosticsPerPage
|
||||||
|
);
|
||||||
|
const pageCount = Math.max(1, Math.ceil(diagnostics.length / pageSize));
|
||||||
|
const page = Math.min(requestedPage, pageCount - 1);
|
||||||
|
const firstDiagnostic = page * pageSize;
|
||||||
|
const limitedTitle = limitUiString(
|
||||||
|
title,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxPanelTitleCharacters
|
||||||
|
);
|
||||||
|
const displayedDiagnostics = diagnostics
|
||||||
|
.slice(firstDiagnostic, firstDiagnostic + pageSize)
|
||||||
|
.map((diagnostic, index) => {
|
||||||
|
const code = limitUiString(
|
||||||
|
diagnostic.code,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxDiagnosticCodeCharacters
|
||||||
|
);
|
||||||
|
const message = limitUiString(
|
||||||
|
diagnostic.message,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxDiagnosticMessageCharacters
|
||||||
|
);
|
||||||
|
const structure =
|
||||||
|
diagnostic.structure === undefined
|
||||||
|
? undefined
|
||||||
|
: limitUiString(
|
||||||
|
diagnostic.structure,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxDiagnosticContextCharacters
|
||||||
|
);
|
||||||
|
const propertyId =
|
||||||
|
diagnostic.propertyId === undefined
|
||||||
|
? undefined
|
||||||
|
: limitUiString(
|
||||||
|
diagnostic.propertyId,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxDiagnosticContextCharacters
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
diagnostic,
|
||||||
|
index: firstDiagnostic + index,
|
||||||
|
code: code.value,
|
||||||
|
message: message.value,
|
||||||
|
structure: structure?.value,
|
||||||
|
propertyId: propertyId?.value,
|
||||||
|
truncated:
|
||||||
|
code.truncated ||
|
||||||
|
message.truncated ||
|
||||||
|
structure?.truncated === true ||
|
||||||
|
propertyId?.truncated === true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const textTruncated =
|
||||||
|
limitedTitle.truncated ||
|
||||||
|
displayedDiagnostics.some((diagnostic) => diagnostic.truncated);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="diagnostics" aria-labelledby="diagnostics-title">
|
<section className="diagnostics" aria-labelledby="diagnostics-title">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Parser details</p>
|
<p className="eyebrow">Parser details</p>
|
||||||
<h2 id="diagnostics-title">{title}</h2>
|
<h2 id="diagnostics-title">{limitedTitle.value}</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className="count-badge">{diagnostics.length}</span>
|
<span className="count-badge">{diagnostics.length}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,36 +91,36 @@ export function DiagnosticsPanel({
|
|||||||
<p className="empty-message">No parser diagnostics for this view.</p>
|
<p className="empty-message">No parser diagnostics for this view.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="diagnostic-list">
|
<ul className="diagnostic-list">
|
||||||
{diagnostics.map((diagnostic, index) => (
|
{displayedDiagnostics.map((displayed) => (
|
||||||
<li
|
<li
|
||||||
className={`diagnostic diagnostic--${diagnostic.severity}`}
|
className={`diagnostic diagnostic--${displayed.diagnostic.severity}`}
|
||||||
key={`${diagnostic.code}-${diagnostic.offset ?? 'none'}-${index}`}
|
key={displayed.index}
|
||||||
>
|
>
|
||||||
<div className="diagnostic__summary">
|
<div className="diagnostic__summary">
|
||||||
<strong>{diagnostic.code}</strong>
|
<strong>{displayed.code}</strong>
|
||||||
<span>{diagnostic.severity}</span>
|
<span>{displayed.diagnostic.severity}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{diagnostic.message}</p>
|
<p>{displayed.message}</p>
|
||||||
{diagnostic.structure !== undefined ||
|
{displayed.structure !== undefined ||
|
||||||
diagnostic.offset !== undefined ||
|
displayed.diagnostic.offset !== undefined ||
|
||||||
diagnostic.propertyId !== undefined ? (
|
displayed.propertyId !== undefined ? (
|
||||||
<dl className="diagnostic__context">
|
<dl className="diagnostic__context">
|
||||||
{diagnostic.structure !== undefined ? (
|
{displayed.structure !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<dt>Structure</dt>
|
<dt>Structure</dt>
|
||||||
<dd>{diagnostic.structure}</dd>
|
<dd>{displayed.structure}</dd>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{diagnostic.offset !== undefined ? (
|
{displayed.diagnostic.offset !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<dt>Offset</dt>
|
<dt>Offset</dt>
|
||||||
<dd>{formatOffset(diagnostic.offset)}</dd>
|
<dd>{formatOffset(displayed.diagnostic.offset)}</dd>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{diagnostic.propertyId !== undefined ? (
|
{displayed.propertyId !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<dt>Property</dt>
|
<dt>Property</dt>
|
||||||
<dd>{diagnostic.propertyId}</dd>
|
<dd>{displayed.propertyId}</dd>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</dl>
|
</dl>
|
||||||
@@ -65,6 +129,41 @@ export function DiagnosticsPanel({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
{diagnostics.length > 0 ? (
|
||||||
|
<nav className="panel-heading" aria-label="Diagnostic pages">
|
||||||
|
<p>
|
||||||
|
Showing diagnostics {firstDiagnostic + 1}–
|
||||||
|
{firstDiagnostic + displayedDiagnostics.length} of{' '}
|
||||||
|
{diagnostics.length}.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page === 0}
|
||||||
|
onClick={() => setRequestedPage(Math.max(0, page - 1))}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>{' '}
|
||||||
|
<span>
|
||||||
|
Page {page + 1} of {pageCount}
|
||||||
|
</span>{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page + 1 >= pageCount}
|
||||||
|
onClick={() =>
|
||||||
|
setRequestedPage(Math.min(pageCount - 1, page + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
) : null}
|
||||||
|
{textTruncated ? (
|
||||||
|
<p className="render-limit-notice" role="status">
|
||||||
|
Long diagnostic text was shortened for safe browser display.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { OneNotePackageDto } from '../onenote/model/dto.js';
|
import type { OneNotePackageDto } from '../onenote/model/dto.js';
|
||||||
|
import {
|
||||||
|
DEFAULT_UI_RENDER_LIMITS,
|
||||||
|
limitUiString,
|
||||||
|
resolveUiLimit,
|
||||||
|
} from './ui-render-budget.js';
|
||||||
|
|
||||||
interface PackageInspectorProps {
|
interface PackageInspectorProps {
|
||||||
notebook: OneNotePackageDto;
|
notebook: OneNotePackageDto;
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(value: number): string {
|
function formatBytes(value: number): string {
|
||||||
@@ -16,14 +24,56 @@ function formatBytes(value: number): string {
|
|||||||
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
|
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PackageInspector({ notebook }: PackageInspectorProps) {
|
export function PackageInspector({
|
||||||
const totalExpanded = notebook.entries.reduce(
|
notebook,
|
||||||
(total, entry) => total + entry.uncompressedSize,
|
pageSize: requestedPageSize,
|
||||||
0
|
}: PackageInspectorProps) {
|
||||||
|
const [requestedPage, setRequestedPage] = useState(0);
|
||||||
|
const pageSize = resolveUiLimit(
|
||||||
|
requestedPageSize,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxPackageEntriesPerPage
|
||||||
);
|
);
|
||||||
const sectionCount = notebook.entries.filter(
|
const pageCount = Math.max(1, Math.ceil(notebook.entries.length / pageSize));
|
||||||
(entry) => entry.kind === 'section'
|
const page = Math.min(requestedPage, pageCount - 1);
|
||||||
).length;
|
const firstEntry = page * pageSize;
|
||||||
|
const displayedEntries = notebook.entries
|
||||||
|
.slice(firstEntry, firstEntry + pageSize)
|
||||||
|
.map((entry, index) => {
|
||||||
|
const normalizedPath = limitUiString(
|
||||||
|
entry.normalizedPath,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxPackagePathCharacters
|
||||||
|
);
|
||||||
|
const originalPath = limitUiString(
|
||||||
|
entry.path,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxPackagePathCharacters
|
||||||
|
);
|
||||||
|
const compressionMethod = limitUiString(
|
||||||
|
entry.compressionMethod || 'unknown',
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxPackageMethodCharacters
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
index: firstEntry + index,
|
||||||
|
normalizedPath: normalizedPath.value,
|
||||||
|
originalPath: originalPath.value,
|
||||||
|
compressionMethod: compressionMethod.value,
|
||||||
|
truncated:
|
||||||
|
normalizedPath.truncated ||
|
||||||
|
originalPath.truncated ||
|
||||||
|
compressionMethod.truncated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const textTruncated = displayedEntries.some((entry) => entry.truncated);
|
||||||
|
|
||||||
|
let totalExpanded = 0;
|
||||||
|
let sectionCount = 0;
|
||||||
|
for (const entry of notebook.entries) {
|
||||||
|
if (entry.kind === 'section') sectionCount += 1;
|
||||||
|
const size = Number.isFinite(entry.uncompressedSize)
|
||||||
|
? Math.max(0, entry.uncompressedSize)
|
||||||
|
: 0;
|
||||||
|
totalExpanded = Math.min(Number.MAX_SAFE_INTEGER, totalExpanded + size);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="package-inspector" aria-labelledby="package-title">
|
<section className="package-inspector" aria-labelledby="package-title">
|
||||||
@@ -66,20 +116,22 @@ export function PackageInspector({ notebook }: PackageInspectorProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{notebook.entries.map((entry) => (
|
{displayedEntries.map((displayed) => (
|
||||||
<tr key={entry.normalizedPath}>
|
<tr key={displayed.index}>
|
||||||
<td>
|
<td>
|
||||||
<code>{entry.normalizedPath}</code>
|
<code>{displayed.normalizedPath}</code>
|
||||||
{entry.path !== entry.normalizedPath ? (
|
{displayed.entry.path !== displayed.entry.normalizedPath ? (
|
||||||
<small>Original: {entry.path}</small>
|
<small>Original: {displayed.originalPath}</small>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td>{entry.kind}</td>
|
<td>{displayed.entry.kind}</td>
|
||||||
<td>{formatBytes(entry.uncompressedSize)}</td>
|
<td>{formatBytes(displayed.entry.uncompressedSize)}</td>
|
||||||
<td>{entry.compressionMethod || 'unknown'}</td>
|
<td>{displayed.compressionMethod}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`status status--${entry.parseStatus}`}>
|
<span
|
||||||
{entry.parseStatus}
|
className={`status status--${displayed.entry.parseStatus}`}
|
||||||
|
>
|
||||||
|
{displayed.entry.parseStatus}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -87,6 +139,42 @@ export function PackageInspector({ notebook }: PackageInspectorProps) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{notebook.entries.length > 0 ? (
|
||||||
|
<nav className="panel-heading" aria-label="Package entry pages">
|
||||||
|
<p>
|
||||||
|
Showing entries {firstEntry + 1}–
|
||||||
|
{firstEntry + displayedEntries.length} of{' '}
|
||||||
|
{notebook.entries.length}.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page === 0}
|
||||||
|
onClick={() => setRequestedPage(Math.max(0, page - 1))}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>{' '}
|
||||||
|
<span>
|
||||||
|
Page {page + 1} of {pageCount}
|
||||||
|
</span>{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page + 1 >= pageCount}
|
||||||
|
onClick={() =>
|
||||||
|
setRequestedPage(Math.min(pageCount - 1, page + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
) : null}
|
||||||
|
{textTruncated ? (
|
||||||
|
<p className="render-limit-notice" role="status">
|
||||||
|
Long package paths or compression labels were shortened for safe
|
||||||
|
browser display.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,12 +6,21 @@ import type {
|
|||||||
OneNoteSectionDto,
|
OneNoteSectionDto,
|
||||||
} from '../onenote/model/dto.js';
|
} from '../onenote/model/dto.js';
|
||||||
import { buildPageTree, type PageTreeNode } from './page-tree.js';
|
import { buildPageTree, type PageTreeNode } from './page-tree.js';
|
||||||
|
import {
|
||||||
|
DEFAULT_UI_RENDER_LIMITS,
|
||||||
|
limitUiString,
|
||||||
|
prepareNotebookForNavigation,
|
||||||
|
preparePagesForNavigation,
|
||||||
|
resolveUiLimit,
|
||||||
|
type NavigationRenderLimits,
|
||||||
|
} from './ui-render-budget.js';
|
||||||
|
|
||||||
interface PageListProps {
|
interface PageListProps {
|
||||||
pages: OneNotePageSummaryDto[];
|
pages: OneNotePageSummaryDto[];
|
||||||
selectedPageId?: string;
|
selectedPageId?: string;
|
||||||
onSelectPage(pageId: string): void;
|
onSelectPage(pageId: string): void;
|
||||||
nested?: boolean;
|
nested?: boolean;
|
||||||
|
renderLimits?: Partial<NavigationRenderLimits>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PageTreeItems({
|
function PageTreeItems({
|
||||||
@@ -52,19 +61,31 @@ function PageList({
|
|||||||
selectedPageId,
|
selectedPageId,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
nested = false,
|
nested = false,
|
||||||
|
renderLimits,
|
||||||
}: PageListProps) {
|
}: PageListProps) {
|
||||||
if (pages.length === 0) {
|
if (pages.length === 0) {
|
||||||
return <p className="navigation-empty">No pages were extracted.</p>;
|
return <p className="navigation-empty">No pages were extracted.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projection = preparePagesForNavigation(pages, renderLimits);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={`page-list${nested ? ' page-list--nested' : ''}`}>
|
<>
|
||||||
<PageTreeItems
|
<ul className={`page-list${nested ? ' page-list--nested' : ''}`}>
|
||||||
nodes={buildPageTree(pages)}
|
<PageTreeItems
|
||||||
selectedPageId={selectedPageId}
|
nodes={buildPageTree(projection.pages)}
|
||||||
onSelectPage={onSelectPage}
|
selectedPageId={selectedPageId}
|
||||||
/>
|
onSelectPage={onSelectPage}
|
||||||
</ul>
|
/>
|
||||||
|
</ul>
|
||||||
|
{projection.truncated ? (
|
||||||
|
<p className="render-limit-notice" role="status">
|
||||||
|
{projection.pages.length < pages.length
|
||||||
|
? `Navigation display is limited to the first ${projection.pages.length} of ${pages.length} pages. Long titles and previews may also be shortened.`
|
||||||
|
: 'Long page titles or previews were shortened for safe browser display.'}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,24 +93,41 @@ interface SingleSectionNavigationProps {
|
|||||||
section: OneNoteSectionDto;
|
section: OneNoteSectionDto;
|
||||||
selectedPageId?: string;
|
selectedPageId?: string;
|
||||||
onSelectPage(pageId: string): void;
|
onSelectPage(pageId: string): void;
|
||||||
|
renderLimits?: Partial<NavigationRenderLimits>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SingleSectionNavigation({
|
export function SingleSectionNavigation({
|
||||||
section,
|
section,
|
||||||
selectedPageId,
|
selectedPageId,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
|
renderLimits,
|
||||||
}: SingleSectionNavigationProps) {
|
}: SingleSectionNavigationProps) {
|
||||||
|
const titleLimit = resolveUiLimit(
|
||||||
|
renderLimits?.maxNavigationTitleCharacters,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNavigationTitleCharacters
|
||||||
|
);
|
||||||
|
const heading = limitUiString(
|
||||||
|
section.sectionName || section.sourceName,
|
||||||
|
titleLimit
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="source-navigation" aria-label="Section pages">
|
<nav className="source-navigation" aria-label="Section pages">
|
||||||
<div className="navigation-heading">
|
<div className="navigation-heading">
|
||||||
<p className="eyebrow">Section</p>
|
<p className="eyebrow">Section</p>
|
||||||
<h2>{section.sectionName || section.sourceName}</h2>
|
<h2>{heading.value}</h2>
|
||||||
<p>{section.pages.length} pages</p>
|
<p>{section.pages.length} pages</p>
|
||||||
</div>
|
</div>
|
||||||
|
{heading.truncated ? (
|
||||||
|
<p className="render-limit-notice" role="status">
|
||||||
|
The section name was shortened for safe browser display.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<PageList
|
<PageList
|
||||||
pages={section.pages}
|
pages={section.pages}
|
||||||
selectedPageId={selectedPageId}
|
selectedPageId={selectedPageId}
|
||||||
onSelectPage={onSelectPage}
|
onSelectPage={onSelectPage}
|
||||||
|
renderLimits={renderLimits}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
@@ -102,6 +140,7 @@ interface PackageNavigationProps {
|
|||||||
selectedPageId?: string;
|
selectedPageId?: string;
|
||||||
onSelectSection(sectionId: string): void;
|
onSelectSection(sectionId: string): void;
|
||||||
onSelectPage(pageId: string): void;
|
onSelectPage(pageId: string): void;
|
||||||
|
renderLimits?: Partial<NavigationRenderLimits>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotebookTreeItems({
|
function NotebookTreeItems({
|
||||||
@@ -111,6 +150,7 @@ function NotebookTreeItems({
|
|||||||
selectedPageId,
|
selectedPageId,
|
||||||
onSelectSection,
|
onSelectSection,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
|
renderLimits,
|
||||||
}: {
|
}: {
|
||||||
nodes: OneNoteNotebookNodeDto[];
|
nodes: OneNoteNotebookNodeDto[];
|
||||||
sections: ReadonlyMap<string, OneNotePackagedSectionDto>;
|
sections: ReadonlyMap<string, OneNotePackagedSectionDto>;
|
||||||
@@ -118,6 +158,7 @@ function NotebookTreeItems({
|
|||||||
selectedPageId?: string;
|
selectedPageId?: string;
|
||||||
onSelectSection(sectionId: string): void;
|
onSelectSection(sectionId: string): void;
|
||||||
onSelectPage(pageId: string): void;
|
onSelectPage(pageId: string): void;
|
||||||
|
renderLimits?: Partial<NavigationRenderLimits>;
|
||||||
}) {
|
}) {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
if (node.kind === 'section-group') {
|
if (node.kind === 'section-group') {
|
||||||
@@ -133,6 +174,7 @@ function NotebookTreeItems({
|
|||||||
selectedPageId={selectedPageId}
|
selectedPageId={selectedPageId}
|
||||||
onSelectSection={onSelectSection}
|
onSelectSection={onSelectSection}
|
||||||
onSelectPage={onSelectPage}
|
onSelectPage={onSelectPage}
|
||||||
|
renderLimits={renderLimits}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
@@ -147,12 +189,12 @@ function NotebookTreeItems({
|
|||||||
<li className="notebook-section" key={node.id}>
|
<li className="notebook-section" key={node.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`${section.displayName}, ${section.parseStatus}`}
|
aria-label={`${node.displayName}, ${section.parseStatus}`}
|
||||||
className={selected ? 'is-selected' : undefined}
|
className={selected ? 'is-selected' : undefined}
|
||||||
aria-current={selected ? 'true' : undefined}
|
aria-current={selected ? 'true' : undefined}
|
||||||
onClick={() => onSelectSection(section.id)}
|
onClick={() => onSelectSection(section.id)}
|
||||||
>
|
>
|
||||||
<strong>{section.displayName}</strong>
|
<strong>{node.displayName}</strong>
|
||||||
<span className={`status status--${section.parseStatus}`}>
|
<span className={`status status--${section.parseStatus}`}>
|
||||||
{section.parseStatus}
|
{section.parseStatus}
|
||||||
</span>
|
</span>
|
||||||
@@ -163,6 +205,7 @@ function NotebookTreeItems({
|
|||||||
selectedPageId={selectedPageId}
|
selectedPageId={selectedPageId}
|
||||||
onSelectPage={onSelectPage}
|
onSelectPage={onSelectPage}
|
||||||
nested
|
nested
|
||||||
|
renderLimits={renderLimits}
|
||||||
/>
|
/>
|
||||||
) : selected ? (
|
) : selected ? (
|
||||||
<p className="navigation-empty">
|
<p className="navigation-empty">
|
||||||
@@ -181,10 +224,15 @@ export function PackageNavigation({
|
|||||||
selectedPageId,
|
selectedPageId,
|
||||||
onSelectSection,
|
onSelectSection,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
|
renderLimits,
|
||||||
}: PackageNavigationProps) {
|
}: PackageNavigationProps) {
|
||||||
const sectionById = new Map(
|
const projection = prepareNotebookForNavigation(tree.nodes, renderLimits);
|
||||||
sections.map((section) => [section.id, section] as const)
|
const sectionById = new Map<string, OneNotePackagedSectionDto>();
|
||||||
);
|
for (const section of sections) {
|
||||||
|
if (projection.sectionIds.has(section.id)) {
|
||||||
|
sectionById.set(section.id, section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="source-navigation" aria-label="Notebook sections and pages">
|
<nav className="source-navigation" aria-label="Notebook sections and pages">
|
||||||
@@ -193,20 +241,27 @@ export function PackageNavigation({
|
|||||||
<h2>Notebook contents</h2>
|
<h2>Notebook contents</h2>
|
||||||
<p>{tree.interpreted ? 'OneNote notebook order' : 'Package order'}</p>
|
<p>{tree.interpreted ? 'OneNote notebook order' : 'Package order'}</p>
|
||||||
</div>
|
</div>
|
||||||
{tree.nodes.length === 0 ? (
|
{projection.nodes.length === 0 ? (
|
||||||
<p className="navigation-empty">No section entries were found.</p>
|
<p className="navigation-empty">No section entries were found.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="notebook-tree section-list">
|
<ul className="notebook-tree section-list">
|
||||||
<NotebookTreeItems
|
<NotebookTreeItems
|
||||||
nodes={tree.nodes}
|
nodes={projection.nodes}
|
||||||
sections={sectionById}
|
sections={sectionById}
|
||||||
selectedSectionId={selectedSectionId}
|
selectedSectionId={selectedSectionId}
|
||||||
selectedPageId={selectedPageId}
|
selectedPageId={selectedPageId}
|
||||||
onSelectSection={onSelectSection}
|
onSelectSection={onSelectSection}
|
||||||
onSelectPage={onSelectPage}
|
onSelectPage={onSelectPage}
|
||||||
|
renderLimits={renderLimits}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
{projection.truncated ? (
|
||||||
|
<p className="render-limit-notice" role="status">
|
||||||
|
Notebook navigation was shortened to a safe browser display budget
|
||||||
|
after {projection.nodeCount} nodes.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
189
src/components/ui-render-budget.test.tsx
Normal file
189
src/components/ui-render-budget.test.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
OneNoteNotebookNodeDto,
|
||||||
|
OneNotePackageDto,
|
||||||
|
OneNotePageSummaryDto,
|
||||||
|
OneNoteSectionDto,
|
||||||
|
} from '../onenote/model/dto.js';
|
||||||
|
import type { ParserDiagnostic } from '../onenote/model/diagnostics.js';
|
||||||
|
import { DiagnosticsPanel } from './DiagnosticsPanel.js';
|
||||||
|
import { PackageInspector } from './PackageInspector.js';
|
||||||
|
import { SingleSectionNavigation } from './SectionNavigation.js';
|
||||||
|
import {
|
||||||
|
prepareNotebookForNavigation,
|
||||||
|
preparePagesForNavigation,
|
||||||
|
} from './ui-render-budget.js';
|
||||||
|
|
||||||
|
describe('bounded UI projections', () => {
|
||||||
|
it('limits page count and display strings before building navigation nodes', () => {
|
||||||
|
const projection = preparePagesForNavigation(
|
||||||
|
[
|
||||||
|
page('first', 'Long title', 'Long preview'),
|
||||||
|
page('second'),
|
||||||
|
page('third'),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
maxNavigationPages: 2,
|
||||||
|
maxNavigationTitleCharacters: 5,
|
||||||
|
maxNavigationPreviewCharacters: 6,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(projection.pages).toHaveLength(2);
|
||||||
|
expect(projection.pages[0]).toMatchObject({
|
||||||
|
title: 'Long…',
|
||||||
|
text: '',
|
||||||
|
textPreview: 'Long …',
|
||||||
|
});
|
||||||
|
expect(projection.truncated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bounds notebook recursion and rejects cyclic DTO branches', () => {
|
||||||
|
const cyclic: OneNoteNotebookNodeDto = {
|
||||||
|
kind: 'section-group',
|
||||||
|
id: 'cycle',
|
||||||
|
displayName: 'Cycle',
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
cyclic.children.push(cyclic);
|
||||||
|
|
||||||
|
const cycleProjection = prepareNotebookForNavigation([cyclic], {
|
||||||
|
maxNotebookNodes: 10,
|
||||||
|
maxNotebookDepth: 10,
|
||||||
|
});
|
||||||
|
expect(cycleProjection.nodes).toEqual([
|
||||||
|
{
|
||||||
|
kind: 'section-group',
|
||||||
|
id: 'cycle',
|
||||||
|
displayName: 'Cycle',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(cycleProjection.truncated).toBe(true);
|
||||||
|
|
||||||
|
const countProjection = prepareNotebookForNavigation(
|
||||||
|
Array.from({ length: 20 }, (_, index) => notebookSection(index)),
|
||||||
|
{ maxNotebookNodes: 3 }
|
||||||
|
);
|
||||||
|
expect(countProjection.nodeCount).toBe(3);
|
||||||
|
expect(countProjection.nodes).toHaveLength(3);
|
||||||
|
expect(countProjection.truncated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders only the configured number of section page links with a notice', () => {
|
||||||
|
render(
|
||||||
|
<SingleSectionNavigation
|
||||||
|
section={section(
|
||||||
|
Array.from({ length: 5 }, (_, index) => page(`${index}`))
|
||||||
|
)}
|
||||||
|
onSelectPage={vi.fn()}
|
||||||
|
renderLimits={{ maxNavigationPages: 2 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getAllByRole('button')).toHaveLength(2);
|
||||||
|
expect(screen.getByText('0')).toBeVisible();
|
||||||
|
expect(screen.queryByText('2')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('status')).toHaveTextContent('first 2 of 5 pages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bounded paged inspectors', () => {
|
||||||
|
it('paginates package entries instead of mounting the complete table', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<PackageInspector notebook={notebook(5)} pageSize={2} />);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByText('Inspect 5 package entries', { selector: 'summary' })
|
||||||
|
);
|
||||||
|
expect(screen.getByText('entry-0.one')).toBeVisible();
|
||||||
|
expect(screen.getByText('entry-1.one')).toBeVisible();
|
||||||
|
expect(screen.queryByText('entry-2.one')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Next' }));
|
||||||
|
expect(screen.queryByText('entry-0.one')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText('entry-2.one')).toBeVisible();
|
||||||
|
expect(screen.getByText('Showing entries 3–4 of 5.')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('paginates diagnostics and truncates the visible message', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const diagnostics = Array.from({ length: 5 }, (_, index) =>
|
||||||
|
diagnostic(index, index === 0 ? 'x'.repeat(10_000) : `message-${index}`)
|
||||||
|
);
|
||||||
|
render(<DiagnosticsPanel diagnostics={diagnostics} pageSize={2} />);
|
||||||
|
|
||||||
|
expect(document.querySelectorAll('.diagnostic')).toHaveLength(2);
|
||||||
|
expect(document.querySelector('.diagnostic p')?.textContent).toHaveLength(
|
||||||
|
4_096
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('message-2')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('status')).toHaveTextContent(
|
||||||
|
'Long diagnostic text was shortened'
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Next' }));
|
||||||
|
expect(document.querySelectorAll('.diagnostic')).toHaveLength(2);
|
||||||
|
expect(screen.getByText('message-2')).toBeVisible();
|
||||||
|
expect(screen.getByText('Showing diagnostics 3–4 of 5.')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function page(id: string, title = id, textPreview = ''): OneNotePageSummaryDto {
|
||||||
|
return { id, title, text: `body-${id}`, textPreview };
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(pages: OneNotePageSummaryDto[]): OneNoteSectionDto {
|
||||||
|
return {
|
||||||
|
format: 'one',
|
||||||
|
sourceName: 'section.one',
|
||||||
|
sectionName: 'Section',
|
||||||
|
pages,
|
||||||
|
diagnostics: [],
|
||||||
|
parserInfo: {
|
||||||
|
implementation: 'typescript',
|
||||||
|
supportedVariant: 'test',
|
||||||
|
referenceRevision: 'test',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function notebookSection(index: number): OneNoteNotebookNodeDto {
|
||||||
|
return {
|
||||||
|
kind: 'section',
|
||||||
|
id: `node-${index}`,
|
||||||
|
sectionId: `section-${index}`,
|
||||||
|
displayName: `Section ${index}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function notebook(entryCount: number): OneNotePackageDto {
|
||||||
|
return {
|
||||||
|
format: 'onepkg',
|
||||||
|
sourceName: 'notebook.onepkg',
|
||||||
|
sourceSize: 100,
|
||||||
|
entries: Array.from({ length: entryCount }, (_, index) => ({
|
||||||
|
path: `entry-${index}.one`,
|
||||||
|
normalizedPath: `entry-${index}.one`,
|
||||||
|
kind: 'section' as const,
|
||||||
|
uncompressedSize: 10,
|
||||||
|
compressionMethod: 'MSZIP',
|
||||||
|
parseStatus: 'parsed' as const,
|
||||||
|
})),
|
||||||
|
sections: [],
|
||||||
|
tree: { interpreted: false, nodes: [] },
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnostic(index: number, message: string): ParserDiagnostic {
|
||||||
|
return {
|
||||||
|
severity: 'warning',
|
||||||
|
code: `TEST_${index}`,
|
||||||
|
message,
|
||||||
|
recoverable: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
213
src/components/ui-render-budget.ts
Normal file
213
src/components/ui-render-budget.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import type {
|
||||||
|
OneNoteNotebookNodeDto,
|
||||||
|
OneNotePageSummaryDto,
|
||||||
|
} from '../onenote/model/dto.js';
|
||||||
|
|
||||||
|
export interface UiRenderLimits {
|
||||||
|
maxNavigationPages: number;
|
||||||
|
maxNotebookNodes: number;
|
||||||
|
maxNotebookDepth: number;
|
||||||
|
maxNavigationTitleCharacters: number;
|
||||||
|
maxNavigationPreviewCharacters: number;
|
||||||
|
maxPackageEntriesPerPage: number;
|
||||||
|
maxPackagePathCharacters: number;
|
||||||
|
maxPackageMethodCharacters: number;
|
||||||
|
maxDiagnosticsPerPage: number;
|
||||||
|
maxDiagnosticCodeCharacters: number;
|
||||||
|
maxDiagnosticMessageCharacters: number;
|
||||||
|
maxDiagnosticContextCharacters: number;
|
||||||
|
maxPanelTitleCharacters: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_UI_RENDER_LIMITS: Readonly<UiRenderLimits> = {
|
||||||
|
maxNavigationPages: 250,
|
||||||
|
maxNotebookNodes: 500,
|
||||||
|
maxNotebookDepth: 16,
|
||||||
|
maxNavigationTitleCharacters: 512,
|
||||||
|
maxNavigationPreviewCharacters: 1_000,
|
||||||
|
maxPackageEntriesPerPage: 200,
|
||||||
|
maxPackagePathCharacters: 1_024,
|
||||||
|
maxPackageMethodCharacters: 128,
|
||||||
|
maxDiagnosticsPerPage: 100,
|
||||||
|
maxDiagnosticCodeCharacters: 128,
|
||||||
|
maxDiagnosticMessageCharacters: 4_096,
|
||||||
|
maxDiagnosticContextCharacters: 512,
|
||||||
|
maxPanelTitleCharacters: 512,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationRenderLimits = Pick<
|
||||||
|
UiRenderLimits,
|
||||||
|
| 'maxNavigationPages'
|
||||||
|
| 'maxNotebookNodes'
|
||||||
|
| 'maxNotebookDepth'
|
||||||
|
| 'maxNavigationTitleCharacters'
|
||||||
|
| 'maxNavigationPreviewCharacters'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface PageNavigationProjection {
|
||||||
|
pages: OneNotePageSummaryDto[];
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotebookNavigationProjection {
|
||||||
|
nodes: OneNoteNotebookNodeDto[];
|
||||||
|
sectionIds: ReadonlySet<string>;
|
||||||
|
nodeCount: number;
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Limit a caller-provided value without permitting the safety cap to grow. */
|
||||||
|
export function resolveUiLimit(
|
||||||
|
value: number | undefined,
|
||||||
|
safetyCap: number
|
||||||
|
): number {
|
||||||
|
if (value === undefined || !Number.isSafeInteger(value) || value < 1) {
|
||||||
|
return safetyCap;
|
||||||
|
}
|
||||||
|
return Math.min(value, safetyCap);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function limitUiString(
|
||||||
|
value: string,
|
||||||
|
maxCharacters: number
|
||||||
|
): { value: string; truncated: boolean } {
|
||||||
|
const limit = Math.max(1, Math.trunc(maxCharacters));
|
||||||
|
if (value.length <= limit) return { value, truncated: false };
|
||||||
|
return {
|
||||||
|
value: `${value.slice(0, Math.max(0, limit - 1))}…`,
|
||||||
|
truncated: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project page summaries before React builds a nested page list. */
|
||||||
|
export function preparePagesForNavigation(
|
||||||
|
pages: readonly OneNotePageSummaryDto[],
|
||||||
|
overrides: Partial<NavigationRenderLimits> = {}
|
||||||
|
): PageNavigationProjection {
|
||||||
|
const maxPages = resolveUiLimit(
|
||||||
|
overrides.maxNavigationPages,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNavigationPages
|
||||||
|
);
|
||||||
|
const maxTitleCharacters = resolveUiLimit(
|
||||||
|
overrides.maxNavigationTitleCharacters,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNavigationTitleCharacters
|
||||||
|
);
|
||||||
|
const maxPreviewCharacters = resolveUiLimit(
|
||||||
|
overrides.maxNavigationPreviewCharacters,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNavigationPreviewCharacters
|
||||||
|
);
|
||||||
|
const projected: OneNotePageSummaryDto[] = [];
|
||||||
|
let truncated = pages.length > maxPages;
|
||||||
|
|
||||||
|
for (let index = 0; index < pages.length && index < maxPages; index += 1) {
|
||||||
|
const page = pages[index]!;
|
||||||
|
const title = limitUiString(page.title, maxTitleCharacters);
|
||||||
|
const preview = limitUiString(page.textPreview, maxPreviewCharacters);
|
||||||
|
truncated ||= title.truncated || preview.truncated;
|
||||||
|
projected.push({
|
||||||
|
id: page.id,
|
||||||
|
level: page.level,
|
||||||
|
title: title.value,
|
||||||
|
text: '',
|
||||||
|
textPreview: preview.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pages: projected, truncated };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project the notebook hierarchy with aggregate node and recursion budgets. */
|
||||||
|
export function prepareNotebookForNavigation(
|
||||||
|
nodes: readonly OneNoteNotebookNodeDto[],
|
||||||
|
overrides: Partial<NavigationRenderLimits> = {}
|
||||||
|
): NotebookNavigationProjection {
|
||||||
|
const limits = {
|
||||||
|
maxNotebookNodes: resolveUiLimit(
|
||||||
|
overrides.maxNotebookNodes,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNotebookNodes
|
||||||
|
),
|
||||||
|
maxNotebookDepth: resolveUiLimit(
|
||||||
|
overrides.maxNotebookDepth,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNotebookDepth
|
||||||
|
),
|
||||||
|
maxNavigationTitleCharacters: resolveUiLimit(
|
||||||
|
overrides.maxNavigationTitleCharacters,
|
||||||
|
DEFAULT_UI_RENDER_LIMITS.maxNavigationTitleCharacters
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const budget = {
|
||||||
|
count: 0,
|
||||||
|
truncated: false,
|
||||||
|
active: new Set<OneNoteNotebookNodeDto>(),
|
||||||
|
sectionIds: new Set<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const projected = projectNotebookNodes(nodes, 0, limits, budget);
|
||||||
|
return {
|
||||||
|
nodes: projected,
|
||||||
|
sectionIds: budget.sectionIds,
|
||||||
|
nodeCount: budget.count,
|
||||||
|
truncated: budget.truncated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectNotebookNodes(
|
||||||
|
nodes: readonly OneNoteNotebookNodeDto[],
|
||||||
|
depth: number,
|
||||||
|
limits: Pick<
|
||||||
|
NavigationRenderLimits,
|
||||||
|
'maxNotebookNodes' | 'maxNotebookDepth' | 'maxNavigationTitleCharacters'
|
||||||
|
>,
|
||||||
|
budget: {
|
||||||
|
count: number;
|
||||||
|
truncated: boolean;
|
||||||
|
active: Set<OneNoteNotebookNodeDto>;
|
||||||
|
sectionIds: Set<string>;
|
||||||
|
}
|
||||||
|
): OneNoteNotebookNodeDto[] {
|
||||||
|
const projected: OneNoteNotebookNodeDto[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (budget.count >= limits.maxNotebookNodes) {
|
||||||
|
budget.truncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (budget.active.has(node)) {
|
||||||
|
budget.truncated = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
budget.count += 1;
|
||||||
|
const displayName = limitUiString(
|
||||||
|
node.displayName,
|
||||||
|
limits.maxNavigationTitleCharacters
|
||||||
|
);
|
||||||
|
budget.truncated ||= displayName.truncated;
|
||||||
|
|
||||||
|
if (node.kind === 'section') {
|
||||||
|
budget.sectionIds.add(node.sectionId);
|
||||||
|
projected.push({
|
||||||
|
kind: 'section',
|
||||||
|
id: node.id,
|
||||||
|
sectionId: node.sectionId,
|
||||||
|
displayName: displayName.value,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
budget.active.add(node);
|
||||||
|
let children: OneNoteNotebookNodeDto[] = [];
|
||||||
|
if (depth >= limits.maxNotebookDepth) {
|
||||||
|
if (node.children.length > 0) budget.truncated = true;
|
||||||
|
} else {
|
||||||
|
children = projectNotebookNodes(node.children, depth + 1, limits, budget);
|
||||||
|
}
|
||||||
|
budget.active.delete(node);
|
||||||
|
projected.push({
|
||||||
|
kind: 'section-group',
|
||||||
|
id: node.id,
|
||||||
|
displayName: displayName.value,
|
||||||
|
children,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return projected;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user