mirror of
https://github.com/jzillmann/pdf-to-markdown.git
synced 2024-11-30 19:54:23 +01:00
PageControls
This commit is contained in:
parent
8b7ae34bbc
commit
45355a9315
@ -12,14 +12,22 @@ export default class Debugger {
|
|||||||
private context: TransformContext;
|
private context: TransformContext;
|
||||||
private transformers: ItemTransformer[];
|
private transformers: ItemTransformer[];
|
||||||
private stageResultCache: StageResult[];
|
private stageResultCache: StageResult[];
|
||||||
|
pageCount: number;
|
||||||
fontMap: Map<string, object>;
|
fontMap: Map<string, object>;
|
||||||
stageNames: string[];
|
stageNames: string[];
|
||||||
stageDescriptions: string[];
|
stageDescriptions: string[];
|
||||||
|
|
||||||
constructor(inputSchema: string[], inputItems: Item[], context: TransformContext, transformers: ItemTransformer[]) {
|
constructor(
|
||||||
|
pageCount: number,
|
||||||
|
inputSchema: string[],
|
||||||
|
inputItems: Item[],
|
||||||
|
context: TransformContext,
|
||||||
|
transformers: ItemTransformer[],
|
||||||
|
) {
|
||||||
this.transformers = transformers;
|
this.transformers = transformers;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.fontMap = context.fontMap;
|
this.fontMap = context.fontMap;
|
||||||
|
this.pageCount = pageCount;
|
||||||
this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)];
|
this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)];
|
||||||
this.stageDescriptions = ['Initial items as parsed by PDFjs', ...transformers.map((t) => t.description)];
|
this.stageDescriptions = ['Initial items as parsed by PDFjs', ...transformers.map((t) => t.description)];
|
||||||
this.stageResultCache = [initialStage(inputSchema, inputItems)];
|
this.stageResultCache = [initialStage(inputSchema, inputItems)];
|
||||||
|
@ -3,30 +3,13 @@ import type Metadata from './Metadata';
|
|||||||
import type PageViewport from './parse/PageViewport';
|
import type PageViewport from './parse/PageViewport';
|
||||||
|
|
||||||
export default class ParseResult {
|
export default class ParseResult {
|
||||||
fontMap: Map<string, object>;
|
|
||||||
pdfjsPages: any[];
|
|
||||||
pageViewports: PageViewport[];
|
|
||||||
metadata: Metadata;
|
|
||||||
schema: string[];
|
|
||||||
items: Item[];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
fontMap: Map<string, object>,
|
public fontMap: Map<string, object>,
|
||||||
pdfjsPages: any[],
|
public pageCount: number,
|
||||||
pageViewports: PageViewport[],
|
public pdfjsPages: any[],
|
||||||
metadata: Metadata,
|
public pageViewports: PageViewport[],
|
||||||
schema: string[],
|
public metadata: Metadata,
|
||||||
items: Item[],
|
public schema: string[],
|
||||||
) {
|
public items: Item[],
|
||||||
this.fontMap = fontMap;
|
) {}
|
||||||
this.pdfjsPages = pdfjsPages;
|
|
||||||
this.pageViewports = pageViewports;
|
|
||||||
this.metadata = metadata;
|
|
||||||
this.schema = schema;
|
|
||||||
this.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pageCount(): number {
|
|
||||||
return this.pdfjsPages.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ export default class PdfParser {
|
|||||||
.promise.then((pdfjsDocument: any) => {
|
.promise.then((pdfjsDocument: any) => {
|
||||||
reporter.parsedDocumentHeader(pdfjsDocument.numPages);
|
reporter.parsedDocumentHeader(pdfjsDocument.numPages);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
pdfjsDocument,
|
||||||
pdfjsDocument.getMetadata().then((pdfjsMetadata: any) => {
|
pdfjsDocument.getMetadata().then((pdfjsMetadata: any) => {
|
||||||
reporter.parsedMetadata();
|
reporter.parsedMetadata();
|
||||||
return new Metadata(pdfjsMetadata);
|
return new Metadata(pdfjsMetadata);
|
||||||
@ -30,10 +31,15 @@ export default class PdfParser {
|
|||||||
this.extractPagesSequentially(pdfjsDocument, reporter),
|
this.extractPagesSequentially(pdfjsDocument, reporter),
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
.then(([metadata, pages]: [Metadata, ParsedPage[]]) => {
|
.then(([pdfjsDocument, metadata, pages]: [any, Metadata, ParsedPage[]]) => {
|
||||||
return Promise.all([metadata, pages, this.gatherFontObjects(pages).finally(() => reporter.parsedFonts())]);
|
return Promise.all([
|
||||||
|
pdfjsDocument,
|
||||||
|
metadata,
|
||||||
|
pages,
|
||||||
|
this.gatherFontObjects(pages).finally(() => reporter.parsedFonts()),
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
.then(([metadata, pages, fontMap]: [Metadata, ParsedPage[], Map<string, object>]) => {
|
.then(([pdfjsDocument, metadata, pages, fontMap]: [any, Metadata, ParsedPage[], Map<string, object>]) => {
|
||||||
const pdfjsPages = pages.map((page: any) => page.pdfjsPage);
|
const pdfjsPages = pages.map((page: any) => page.pdfjsPage);
|
||||||
const items = pages.reduce((allItems: any[], page: any) => allItems.concat(page.items), []);
|
const items = pages.reduce((allItems: any[], page: any) => allItems.concat(page.items), []);
|
||||||
const pageViewports = pdfjsPages.map((page: any) => {
|
const pageViewports = pdfjsPages.map((page: any) => {
|
||||||
@ -43,7 +49,15 @@ export default class PdfParser {
|
|||||||
this.pdfjs.Util.transform(viewPort.transform, itemTransform),
|
this.pdfjs.Util.transform(viewPort.transform, itemTransform),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return new ParseResult(fontMap, pdfjsPages, pageViewports, metadata, this.schema, items);
|
return new ParseResult(
|
||||||
|
fontMap,
|
||||||
|
pdfjsDocument.numPages,
|
||||||
|
pdfjsPages,
|
||||||
|
pageViewports,
|
||||||
|
metadata,
|
||||||
|
this.schema,
|
||||||
|
items,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export default class PdfPipeline {
|
|||||||
async debug(src: string | Uint8Array | object, progressListener: ProgressListenFunction): Promise<Debugger> {
|
async debug(src: string | Uint8Array | object, progressListener: ProgressListenFunction): Promise<Debugger> {
|
||||||
const parseResult = await this.parse(src, progressListener);
|
const parseResult = await this.parse(src, progressListener);
|
||||||
const context = { fontMap: parseResult.fontMap, pageViewports: parseResult.pageViewports };
|
const context = { fontMap: parseResult.fontMap, pageViewports: parseResult.pageViewports };
|
||||||
return new Debugger(parseResult.schema, parseResult.items, context, this.transformers);
|
return new Debugger(parseResult.pageCount, parseResult.schema, parseResult.items, context, this.transformers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,7 @@ export default class StageResult {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectPages(relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number): Page[] {
|
selectPages(relevantChangesOnly: boolean, groupItems: boolean): Page[] {
|
||||||
let result: Page[];
|
let result: Page[];
|
||||||
|
|
||||||
// Ungroup pages
|
// Ungroup pages
|
||||||
@ -32,11 +32,6 @@ export default class StageResult {
|
|||||||
result = this.pages;
|
result = this.pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter to pinned page
|
|
||||||
if (Number.isInteger(pinnedPage)) {
|
|
||||||
result = result.filter((page) => page.index === pinnedPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out item (groups) with no changes
|
// Filter out item (groups) with no changes
|
||||||
if (relevantChangesOnly && !this.descriptor.debug?.showAll === true) {
|
if (relevantChangesOnly && !this.descriptor.debug?.showAll === true) {
|
||||||
result = result.map(
|
result = result.map(
|
||||||
|
@ -36,7 +36,7 @@ describe('Transform Items', () => {
|
|||||||
const trans1Items = parsedItems.map((item) => item.withData({ C: `c=${item.value('A')}+${item.value('B')}` }));
|
const trans1Items = parsedItems.map((item) => item.withData({ C: `c=${item.value('A')}+${item.value('B')}` }));
|
||||||
|
|
||||||
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
||||||
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
||||||
|
|
||||||
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
||||||
expect(debug.stageResults(0).schema).toEqual(parsedSchema.map((column) => ({ name: column })));
|
expect(debug.stageResults(0).schema).toEqual(parsedSchema.map((column) => ({ name: column })));
|
||||||
@ -62,7 +62,7 @@ describe('Transform Items', () => {
|
|||||||
const trans1Items = parsedItems.map((item) => item.withData({ line: item.data['y'] }));
|
const trans1Items = parsedItems.map((item) => item.withData({ line: item.data['y'] }));
|
||||||
|
|
||||||
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
||||||
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
||||||
|
|
||||||
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
||||||
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'y' }]);
|
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'y' }]);
|
||||||
@ -100,7 +100,7 @@ test('Change inside of Line', async () => {
|
|||||||
const trans1Items = swapElements([...parsedItems], 0, 1);
|
const trans1Items = swapElements([...parsedItems], 0, 1);
|
||||||
|
|
||||||
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
||||||
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
||||||
|
|
||||||
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
||||||
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'line' }]);
|
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'line' }]);
|
||||||
@ -134,7 +134,7 @@ describe('build schemas', () => {
|
|||||||
|
|
||||||
function calculateSchema(inputSchema: string[], outputSchema: string[]): AnnotatedColumn[] {
|
function calculateSchema(inputSchema: string[], outputSchema: string[]): AnnotatedColumn[] {
|
||||||
const transformers = [new TestTransformer('Trans1', {}, outputSchema, items)];
|
const transformers = [new TestTransformer('Trans1', {}, outputSchema, items)];
|
||||||
const debug = new Debugger(inputSchema, items, { fontMap: new Map(), pageViewports: [] }, transformers);
|
const debug = new Debugger(1, inputSchema, items, { fontMap: new Map(), pageViewports: [] }, transformers);
|
||||||
return debug.stageResults(1).schema;
|
return debug.stageResults(1).schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ test('basic example PDF parse', async () => {
|
|||||||
const expectedPages = 7;
|
const expectedPages = 7;
|
||||||
expect(result.metadata.title()).toEqual('ExamplePdf');
|
expect(result.metadata.title()).toEqual('ExamplePdf');
|
||||||
expect(result.metadata.author()).toEqual('Johannes Zillmann');
|
expect(result.metadata.author()).toEqual('Johannes Zillmann');
|
||||||
expect(result.pageCount()).toBe(expectedPages);
|
expect(result.pageCount).toBe(expectedPages);
|
||||||
result.pdfjsPages.forEach((pdfPage, i) => {
|
result.pdfjsPages.forEach((pdfPage, i) => {
|
||||||
expect(pdfPage._pageIndex).toBe(i);
|
expect(pdfPage._pageIndex).toBe(i);
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,6 @@ import AnnotatedColumn from 'src/debug/AnnotatedColumn';
|
|||||||
import Page, { asPages } from 'src/debug/Page';
|
import Page, { asPages } from 'src/debug/Page';
|
||||||
import { items } from '../testItems';
|
import { items } from '../testItems';
|
||||||
import LineItemMerger from 'src/debug/LineItemMerger';
|
import LineItemMerger from 'src/debug/LineItemMerger';
|
||||||
import ItemGroup from 'src/debug/ItemGroup';
|
|
||||||
|
|
||||||
test('itemsUnpacked', async () => {
|
test('itemsUnpacked', async () => {
|
||||||
const tracker = new ChangeTracker();
|
const tracker = new ChangeTracker();
|
||||||
@ -77,22 +76,6 @@ describe('select pages', () => {
|
|||||||
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
|
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
|
||||||
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
|
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
|
||||||
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5]]);
|
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5]]);
|
||||||
|
|
||||||
const allGroupedWithPin = result.selectPages(false, true, 0);
|
|
||||||
expect(allGroupedWithPin.map((page) => page.index)).toEqual([0]);
|
|
||||||
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[0, 1], [2]]);
|
|
||||||
|
|
||||||
const relevantGroupedWithPin = result.selectPages(true, true, 0);
|
|
||||||
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([0]);
|
|
||||||
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[0, 1]]);
|
|
||||||
|
|
||||||
const relevantUnpackedWithPin = result.selectPages(true, false, 0);
|
|
||||||
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([0]);
|
|
||||||
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([]);
|
|
||||||
|
|
||||||
const allUnpackedWithPin = result.selectPages(false, false, 0);
|
|
||||||
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([0]);
|
|
||||||
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[0], [1], [2]]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Changes on element level', async () => {
|
test('Changes on element level', async () => {
|
||||||
@ -141,22 +124,6 @@ describe('select pages', () => {
|
|||||||
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
|
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
|
||||||
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
|
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
|
||||||
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5], [6]]);
|
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5], [6]]);
|
||||||
|
|
||||||
const allGroupedWithPin = result.selectPages(false, true, 2);
|
|
||||||
expect(allGroupedWithPin.map((page) => page.index)).toEqual([2]);
|
|
||||||
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[4, 5], [6]]);
|
|
||||||
|
|
||||||
const relevantGroupedWithPin = result.selectPages(true, true, 2);
|
|
||||||
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([2]);
|
|
||||||
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[4, 5]]);
|
|
||||||
|
|
||||||
const relevantUnpackedWithPin = result.selectPages(true, false, 2);
|
|
||||||
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([2]);
|
|
||||||
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([[5]]);
|
|
||||||
|
|
||||||
const allUnpackedWithPin = result.selectPages(false, false, 2);
|
|
||||||
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([2]);
|
|
||||||
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[4], [5], [6]]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('showAll - grouped', async () => {
|
test('showAll - grouped', async () => {
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
|
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
|
||||||
|
|
||||||
import { debugStage } from '../config';
|
import { debugStage } from '../config';
|
||||||
import type StageResult from '@core/debug/StageResult';
|
|
||||||
|
|
||||||
|
import PageControl from './PageControl';
|
||||||
import Popup from '../components/Popup.svelte';
|
import Popup from '../components/Popup.svelte';
|
||||||
import PageSelectionPopup from './PageSelectionPopup.svelte';
|
import PageSelectionPopup from './PageSelectionPopup.svelte';
|
||||||
import Checkbox from '../components/Checkbox.svelte';
|
import Checkbox from '../components/Checkbox.svelte';
|
||||||
@ -18,26 +18,24 @@
|
|||||||
|
|
||||||
export let stageNames: string[];
|
export let stageNames: string[];
|
||||||
export let stageDescriptions: string[];
|
export let stageDescriptions: string[];
|
||||||
|
export let pageControl: PageControl;
|
||||||
export let fontMap: Map<string, object>;
|
export let fontMap: Map<string, object>;
|
||||||
export let stageResult: StageResult;
|
|
||||||
export let supportsGrouping: boolean;
|
export let supportsGrouping: boolean;
|
||||||
export let supportsRelevanceFiltering: boolean;
|
export let supportsRelevanceFiltering: boolean;
|
||||||
|
|
||||||
export let pinnedPage: number;
|
|
||||||
export let groupingEnabled = true;
|
export let groupingEnabled = true;
|
||||||
export let onlyRelevantItems = true;
|
export let onlyRelevantItems = true;
|
||||||
|
|
||||||
|
let { pinnedPageIndex, pagePinned } = pageControl;
|
||||||
|
|
||||||
$: canNext = $debugStage + 1 < stageNames.length;
|
$: canNext = $debugStage + 1 < stageNames.length;
|
||||||
$: canPrev = $debugStage > 0;
|
$: canPrev = $debugStage > 0;
|
||||||
$: pagesNumbers = new Set(stageResult.pages.map((page) => page.index));
|
|
||||||
$: maxPage = Math.max(...pagesNumbers);
|
|
||||||
$: pageIsPinned = !isNaN(pinnedPage);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sticky top-0 pt-2 pb-1 z-20 bg-gray-50">
|
<div class="sticky top-0 pt-2 pb-1 z-20 bg-gray-50">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
{#if pageIsPinned}
|
{#if $pagePinned}
|
||||||
<span on:click={() => (pinnedPage = undefined)} transition:slideH={{ duration: 180, easing: linear }}>
|
<span on:click={() => pageControl.unpinPage()} transition:slideH={{ duration: 180, easing: linear }}>
|
||||||
<Icon class="text-xs hover:text-select hover:opacity-25 cursor-pointer opacity-75" icon={pin} />
|
<Icon class="text-xs hover:text-select hover:opacity-25 cursor-pointer opacity-75" icon={pin} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@ -47,12 +45,7 @@
|
|||||||
<BookOpen size="1x" class="hover:text-select cursor-pointer {opened && 'text-select'}" />
|
<BookOpen size="1x" class="hover:text-select cursor-pointer {opened && 'text-select'}" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="content">
|
<span slot="content">
|
||||||
<PageSelectionPopup
|
<PageSelectionPopup {pageControl} />
|
||||||
{pagesNumbers}
|
|
||||||
{maxPage}
|
|
||||||
{pinnedPage}
|
|
||||||
on:pinPage={(e) => (pinnedPage = e.detail)}
|
|
||||||
on:unpinPage={() => (pinnedPage = undefined)} />
|
|
||||||
</span>
|
</span>
|
||||||
</Popup>
|
</Popup>
|
||||||
</span>
|
</span>
|
||||||
@ -80,10 +73,10 @@
|
|||||||
<div>|</div>
|
<div>|</div>
|
||||||
<div>Transformation:</div>
|
<div>Transformation:</div>
|
||||||
<span on:click={() => canPrev && debugStage.update((cur) => cur - 1)}>
|
<span on:click={() => canPrev && debugStage.update((cur) => cur - 1)}>
|
||||||
<ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
|
<ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
|
||||||
</span>
|
</span>
|
||||||
<span on:click={() => canNext && debugStage.update((cur) => cur + 1)}>
|
<span on:click={() => canNext && debugStage.update((cur) => cur + 1)}>
|
||||||
<ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
|
<ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<Popup>
|
<Popup>
|
||||||
|
@ -7,19 +7,20 @@
|
|||||||
import { debugStage } from '../config';
|
import { debugStage } from '../config';
|
||||||
import ControlBar from './ControlBar.svelte';
|
import ControlBar from './ControlBar.svelte';
|
||||||
import ItemTable from './ItemTable.svelte';
|
import ItemTable from './ItemTable.svelte';
|
||||||
|
import PageControl from './PageControl';
|
||||||
|
|
||||||
export let debug: Debugger;
|
export let debug: Debugger;
|
||||||
|
|
||||||
|
const pageControl = new PageControl(debug.pageCount);
|
||||||
const stageNames = debug.stageNames;
|
const stageNames = debug.stageNames;
|
||||||
let pinnedPage: number;
|
const { pinnedPageIndex } = pageControl;
|
||||||
let groupingEnabled = true;
|
let groupingEnabled = true;
|
||||||
let onlyRelevantItems = true;
|
let onlyRelevantItems = true;
|
||||||
|
|
||||||
$: stageResult = debug.stageResults($debugStage);
|
$: stageResult = debug.stageResults($debugStage);
|
||||||
$: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger;
|
$: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger;
|
||||||
$: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll;
|
$: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll;
|
||||||
$: pageIsPinned = !isNaN(pinnedPage);
|
$: visiblePages = pageControl.selectPages(stageResult, onlyRelevantItems, groupingEnabled, $pinnedPageIndex);
|
||||||
$: visiblePages = stageResult.selectPages(onlyRelevantItems, groupingEnabled, pinnedPage);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-4">
|
<div class="mx-4">
|
||||||
@ -31,13 +32,12 @@
|
|||||||
<ControlBar
|
<ControlBar
|
||||||
{stageNames}
|
{stageNames}
|
||||||
stageDescriptions={debug.stageDescriptions}
|
stageDescriptions={debug.stageDescriptions}
|
||||||
|
{pageControl}
|
||||||
fontMap={debug.fontMap}
|
fontMap={debug.fontMap}
|
||||||
{stageResult}
|
|
||||||
{supportsGrouping}
|
{supportsGrouping}
|
||||||
{supportsRelevanceFiltering}
|
{supportsRelevanceFiltering}
|
||||||
bind:groupingEnabled
|
bind:groupingEnabled
|
||||||
bind:onlyRelevantItems
|
bind:onlyRelevantItems />
|
||||||
bind:pinnedPage />
|
|
||||||
|
|
||||||
<!-- Stage Messages -->
|
<!-- Stage Messages -->
|
||||||
<ul class="messages list-disc list-inside mb-2 p-2 bg-blue-50 rounded shadow text-sm">
|
<ul class="messages list-disc list-inside mb-2 p-2 bg-blue-50 rounded shadow text-sm">
|
||||||
@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<!-- Items -->
|
<!-- Items -->
|
||||||
{#if visiblePages.find((page) => page.itemGroups.length > 0)}
|
{#if visiblePages.find((page) => page.itemGroups.length > 0)}
|
||||||
<ItemTable schema={stageResult.schema} pages={visiblePages} {pageIsPinned} changes={stageResult.changes} />
|
<ItemTable schema={stageResult.schema} pages={visiblePages} {pageControl} changes={stageResult.changes} />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- No items visible -->
|
<!-- No items visible -->
|
||||||
<div class="flex space-x-1 items-center justify-center text-xl">
|
<div class="flex space-x-1 items-center justify-center text-xl">
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { scale, fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
import { ArrowLeft, ArrowRight } from 'svelte-hero-icons';
|
||||||
|
|
||||||
import type ItemGroup from '@core/debug/ItemGroup';
|
import type ItemGroup from '@core/debug/ItemGroup';
|
||||||
import type ChangeIndex from '@core/debug/ChangeIndex';
|
import type ChangeIndex from '@core/debug/ChangeIndex';
|
||||||
@ -7,15 +9,16 @@
|
|||||||
|
|
||||||
import ChangeSymbol from './ChangeSymbol.svelte';
|
import ChangeSymbol from './ChangeSymbol.svelte';
|
||||||
import { formatValue } from './formatValues';
|
import { formatValue } from './formatValues';
|
||||||
|
import PageControl from './PageControl';
|
||||||
|
|
||||||
|
export let pageControl: PageControl;
|
||||||
export let pageIdx: number;
|
export let pageIdx: number;
|
||||||
export let itemIdx: number;
|
export let itemIdx: number;
|
||||||
export let schema: AnnotatedColumn[];
|
export let schema: AnnotatedColumn[];
|
||||||
export let itemGroup: ItemGroup;
|
export let itemGroup: ItemGroup;
|
||||||
export let changes: ChangeIndex;
|
export let changes: ChangeIndex;
|
||||||
export let maxPage: number;
|
|
||||||
export let pageIsPinned: boolean;
|
|
||||||
|
|
||||||
|
let { pagePinned, canPrev, canNext } = pageControl;
|
||||||
let expandedItemGroup: { pageIndex: number; itemIndex: number };
|
let expandedItemGroup: { pageIndex: number; itemIndex: number };
|
||||||
|
|
||||||
$: expanded =
|
$: expanded =
|
||||||
@ -36,7 +39,17 @@
|
|||||||
<!-- Page number in first page item row -->
|
<!-- Page number in first page item row -->
|
||||||
{#if itemIdx === 0}
|
{#if itemIdx === 0}
|
||||||
<td id="page" class="page bg-gray-50 align-top">
|
<td id="page" class="page bg-gray-50 align-top">
|
||||||
<div>Page {pageIdx} {pageIsPinned ? '' : ' / ' + maxPage}</div>
|
<div>Page {pageIdx + 1} {$pagePinned ? '' : ' / ' + pageControl.totalPages}</div>
|
||||||
|
{#if $pagePinned}
|
||||||
|
<div class="absolute flex ml-1 space-x-2">
|
||||||
|
<span on:click={() => pageControl.prev()}>
|
||||||
|
<ArrowLeft size="1x" class={$canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
|
||||||
|
</span>
|
||||||
|
<span on:click={() => pageControl.next()}>
|
||||||
|
<ArrowRight size="1x" class={$canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td id="page" />
|
<td id="page" />
|
||||||
|
@ -7,21 +7,22 @@
|
|||||||
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
|
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
|
||||||
|
|
||||||
import inView from '../actions/inView';
|
import inView from '../actions/inView';
|
||||||
|
import PageControl from './PageControl';
|
||||||
import ItemRow from './ItemRow.svelte';
|
import ItemRow from './ItemRow.svelte';
|
||||||
|
|
||||||
export let schema: AnnotatedColumn[];
|
export let schema: AnnotatedColumn[];
|
||||||
export let pages: Page[];
|
export let pages: Page[];
|
||||||
export let pageIsPinned: boolean;
|
export let pageControl: PageControl;
|
||||||
export let changes: ChangeIndex;
|
export let changes: ChangeIndex;
|
||||||
|
|
||||||
|
let { pagePinned } = pageControl;
|
||||||
let maxPage = pages[pages.length - 1].index;
|
let maxPage = pages[pages.length - 1].index;
|
||||||
let maxItemsToRenderInOneLoad = 200;
|
let maxItemsToRenderInOneLoad = 200;
|
||||||
let renderedMaxPage = 0;
|
let renderedMaxPage = 0;
|
||||||
let expandedItemGroup: { pageIndex: number; itemIndex: number };
|
|
||||||
|
|
||||||
let renderedPages: Page[];
|
let renderedPages: Page[];
|
||||||
$: {
|
$: {
|
||||||
if (pageIsPinned) {
|
if ($pagePinned) {
|
||||||
renderedPages = pages;
|
renderedPages = pages;
|
||||||
renderedMaxPage = 0;
|
renderedMaxPage = 0;
|
||||||
} else {
|
} else {
|
||||||
@ -44,13 +45,6 @@
|
|||||||
}
|
}
|
||||||
// console.log(`Render pages 0 to ${renderedMaxPage} with ${itemCount} items`);
|
// console.log(`Render pages 0 to ${renderedMaxPage} with ${itemCount} items`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExpanded = (pageIndex: number, itemIndex: number) => {
|
|
||||||
return expandedItemGroup?.pageIndex === pageIndex && expandedItemGroup?.itemIndex === itemIndex;
|
|
||||||
};
|
|
||||||
const toggleRow = (pageIndex: number, itemIndex: number) => {
|
|
||||||
expandedItemGroup = isExpanded(pageIndex, itemIndex) ? undefined : { pageIndex, itemIndex };
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Item table -->
|
<!-- Item table -->
|
||||||
@ -77,13 +71,13 @@
|
|||||||
|
|
||||||
<!-- Page items -->
|
<!-- Page items -->
|
||||||
{#each page.itemGroups as itemGroup, itemIdx}
|
{#each page.itemGroups as itemGroup, itemIdx}
|
||||||
<ItemRow {pageIdx} {itemIdx} {schema} {itemGroup} {changes} {maxPage} {pageIsPinned} />
|
<ItemRow pageIdx={page.index} {itemIdx} {schema} {itemGroup} {changes} {pageControl} />
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{#if !pageIsPinned}
|
{#if !pagePinned}
|
||||||
{#if renderedMaxPage < pages.length}
|
{#if renderedMaxPage < pages.length}
|
||||||
<span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} />
|
<span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} />
|
||||||
<div class="my-6 text-center text-2xl">...</div>
|
<div class="my-6 text-center text-2xl">...</div>
|
||||||
|
67
ui/src/debug/PageControl.ts
Normal file
67
ui/src/debug/PageControl.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import Page from '@core/debug/Page';
|
||||||
|
import StageResult from '@core/debug/StageResult';
|
||||||
|
import { Writable, writable, get, Readable, derived } from 'svelte/store';
|
||||||
|
|
||||||
|
export default class PageControl {
|
||||||
|
pinnedPageIndex: Writable<number | undefined>;
|
||||||
|
pagePinned: Readable<boolean>;
|
||||||
|
canPrev: Readable<boolean>;
|
||||||
|
canNext: Readable<boolean>;
|
||||||
|
pagesWithItems: Set<number> = new Set();
|
||||||
|
|
||||||
|
constructor(public totalPages: number) {
|
||||||
|
this.pinnedPageIndex = writable(undefined);
|
||||||
|
this.pagePinned = derived(this.pinnedPageIndex, (page) => !isNaN(page));
|
||||||
|
this.canPrev = derived([this.pinnedPageIndex, this.pagePinned], ([page, pinned]) => pinned && page > 0);
|
||||||
|
this.canNext = derived(
|
||||||
|
[this.pinnedPageIndex, this.pagePinned],
|
||||||
|
([page, pinned]) => pinned && page < totalPages - 1
|
||||||
|
);
|
||||||
|
this.next.bind(this);
|
||||||
|
this.prev.bind(this);
|
||||||
|
this.unpinPage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPages(stageResult: StageResult, relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number) {
|
||||||
|
const allRelevantPages = stageResult.selectPages(relevantChangesOnly, groupItems);
|
||||||
|
this.pagesWithItems = new Set(
|
||||||
|
allRelevantPages.filter((page) => page.itemGroups.length > 0).map((page) => page.index)
|
||||||
|
);
|
||||||
|
if (Number.isInteger(pinnedPage)) {
|
||||||
|
return allRelevantPages.filter((page) => page.index === pinnedPage);
|
||||||
|
}
|
||||||
|
return allRelevantPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageHasItems(pageIdx: number) {
|
||||||
|
return this.pagesWithItems.has(pageIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
if (get(this.canNext)) {
|
||||||
|
this.pinnedPageIndex.update((page) => page + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev() {
|
||||||
|
if (get(this.canPrev)) {
|
||||||
|
this.pinnedPageIndex.update((page) => page - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinPage(pageIdx: number) {
|
||||||
|
this.pinnedPageIndex.set(pageIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpinPage() {
|
||||||
|
this.pinnedPageIndex.set(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedPage() {
|
||||||
|
return get(this.pinnedPageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
pageIsPinned() {
|
||||||
|
return get(this.pagePinned);
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +1,39 @@
|
|||||||
<script>
|
<script>
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
import { Collection } from 'svelte-hero-icons';
|
import { Collection } from 'svelte-hero-icons';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
export let pagesNumbers: Set<number>;
|
import PageControl from './PageControl';
|
||||||
export let maxPage: number;
|
|
||||||
export let pinnedPage: number;
|
export let pageControl: PageControl;
|
||||||
|
|
||||||
|
let { pinnedPageIndex, pagePinned } = pageControl;
|
||||||
|
|
||||||
const popupOpened: Writable<boolean> = getContext('popupOpened');
|
const popupOpened: Writable<boolean> = getContext('popupOpened');
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
function pinPage(index: number) {
|
function pinPage(index: number) {
|
||||||
popupOpened.set(false);
|
popupOpened.set(false);
|
||||||
dispatch('pinPage', index);
|
pageControl.pinPage(index);
|
||||||
}
|
}
|
||||||
function unpinPage() {
|
function unpinPage() {
|
||||||
popupOpened.set(false);
|
popupOpened.set(false);
|
||||||
dispatch('unpinPage');
|
pageControl.unpinPage();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute mt-2 p-2 flex bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96" transition:slide>
|
<div class="absolute mt-2 p-2 flex bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96" transition:slide>
|
||||||
<span class="mt-1 pr-2" on:click={!!pinnedPage && unpinPage}>
|
<span class="mt-1 pr-2" on:click={$pagePinned && unpinPage}>
|
||||||
<Collection size="1x" class={!!pinnedPage ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
|
<Collection size="1x" class={$pagePinned ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
|
||||||
</span>
|
</span>
|
||||||
<div class="grid gap-3" style="grid-template-columns: repeat({Math.min(20, maxPage + 1)}, minmax(0, 1fr));">
|
|
||||||
{#each new Array(maxPage + 1) as _, idx}
|
|
||||||
<div
|
<div
|
||||||
on:click={() => pagesNumbers.has(idx) && pinPage(idx)}
|
class="grid gap-3"
|
||||||
class="px-2 border border-gray-300 rounded-full text-center {pagesNumbers.has(idx) ? (pinnedPage === idx ? 'bg-select' : 'hover:text-select hover:border-select cursor-pointer') : 'opacity-50'}">
|
style="grid-template-columns: repeat({Math.min(20, pageControl.totalPages)}, minmax(0, 1fr));">
|
||||||
{idx}
|
{#each new Array(pageControl.totalPages) as _, idx}
|
||||||
|
<div
|
||||||
|
on:click={() => pageControl.pageHasItems(idx) && pinPage(idx)}
|
||||||
|
class="px-2 border border-gray-300 rounded-full text-center {pageControl.pageHasItems(idx) ? ($pinnedPageIndex === idx ? 'bg-select' : 'hover:text-select hover:border-select cursor-pointer') : 'opacity-50'}">
|
||||||
|
{idx + 1}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user