mirror of
https://github.com/jzillmann/pdf-to-markdown.git
synced 2025-06-25 20:11:39 +02:00
Change detection on group and item level
This commit is contained in:
parent
229cb53eb0
commit
e7574513c5
@ -1,10 +1,12 @@
|
|||||||
import Item from './Item';
|
import Item from './Item';
|
||||||
import ItemTransformer from './transformer/ItemTransformer';
|
import ItemTransformer from './transformer/ItemTransformer';
|
||||||
import TransformContext from './transformer/TransformContext';
|
import TransformContext from './transformer/TransformContext';
|
||||||
import StageResult from './debug/StageResult';
|
import StageResult, { initialStage } from './debug/StageResult';
|
||||||
import ColumnAnnotation from './debug/ColumnAnnotation';
|
import ColumnAnnotation from './debug/ColumnAnnotation';
|
||||||
import AnnotatedColumn from './debug/AnnotatedColumn';
|
import AnnotatedColumn from './debug/AnnotatedColumn';
|
||||||
import { detectChanges } from './debug/detectChanges';
|
import { detectChanges } from './debug/detectChanges';
|
||||||
|
import { asPages } from './debug/Page';
|
||||||
|
import ChangeTracker from './debug/ChangeTracker';
|
||||||
|
|
||||||
export default class Debugger {
|
export default class Debugger {
|
||||||
private context: TransformContext;
|
private context: TransformContext;
|
||||||
@ -16,42 +18,36 @@ export default class Debugger {
|
|||||||
this.transformers = transformers;
|
this.transformers = transformers;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)];
|
this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)];
|
||||||
this.stageResultCache = [
|
this.stageResultCache = [initialStage(inputSchema, inputItems)];
|
||||||
{
|
|
||||||
schema: inputSchema.map((column) => ({ name: column })),
|
|
||||||
items: inputItems,
|
|
||||||
changes: new Map(),
|
|
||||||
messages: [
|
|
||||||
`Parsed ${inputItems.length === 0 ? 0 : inputItems[inputItems.length - 1].page + 1} pages with ${
|
|
||||||
inputItems.length
|
|
||||||
} items`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO return MarkedItem ? (removed, added, etc..)?
|
|
||||||
stageResults(stageIndex: number): StageResult {
|
stageResults(stageIndex: number): StageResult {
|
||||||
for (let idx = 0; idx < stageIndex + 1; idx++) {
|
for (let idx = 0; idx < stageIndex + 1; idx++) {
|
||||||
if (!this.stageResultCache[idx]) {
|
if (!this.stageResultCache[idx]) {
|
||||||
const transformer = this.transformers[idx - 1];
|
const transformer = this.transformers[idx - 1];
|
||||||
const previousStageResult: StageResult = this.stageResultCache[idx - 1];
|
const previousStageResult: StageResult = this.stageResultCache[idx - 1];
|
||||||
|
const previousItems = previousStageResult.itemsUnpacked();
|
||||||
const inputSchema = toSimpleSchema(previousStageResult);
|
const inputSchema = toSimpleSchema(previousStageResult);
|
||||||
const outputSchema = transformer.schemaTransformer(inputSchema);
|
const outputSchema = transformer.schemaTransformer(inputSchema);
|
||||||
const itemResult = transformer.transform(this.context, [...previousStageResult.items]);
|
const itemResult = transformer.transform(this.context, [...previousItems]);
|
||||||
|
|
||||||
const changes = detectChanges(previousStageResult.items, itemResult.items);
|
const changeTracker = new ChangeTracker();
|
||||||
|
detectChanges(changeTracker, previousItems, itemResult.items);
|
||||||
const stageResult = {
|
const pages = asPages(changeTracker, itemResult.items, transformer.descriptor.debug?.itemMerger);
|
||||||
descriptor: transformer.descriptor,
|
const messages = itemResult.messages;
|
||||||
schema: toAnnotatedSchema(inputSchema, outputSchema),
|
if (changeTracker.changeCount() > 0) {
|
||||||
...itemResult,
|
messages.unshift(`Detected ${changeTracker.changeCount()} changes`);
|
||||||
changes,
|
|
||||||
};
|
|
||||||
if (changes.size > 0) {
|
|
||||||
stageResult.messages.unshift(`Detected ${changes.size} changes`);
|
|
||||||
}
|
}
|
||||||
this.stageResultCache.push(stageResult);
|
|
||||||
|
this.stageResultCache.push(
|
||||||
|
new StageResult(
|
||||||
|
transformer.descriptor,
|
||||||
|
toAnnotatedSchema(inputSchema, outputSchema),
|
||||||
|
pages,
|
||||||
|
changeTracker,
|
||||||
|
messages,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.stageResultCache[stageIndex];
|
return this.stageResultCache[stageIndex];
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type ItemMerger from './support/ItemMerger';
|
import type ItemMerger from './debug/ItemMerger';
|
||||||
|
|
||||||
interface Debug {
|
interface Debug {
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,12 @@ export function assert(condition: boolean, message: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertNot(condition: boolean, message: string) {
|
||||||
|
if (condition) {
|
||||||
|
throw new Error(message || 'Assertion failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function assertDefined<T>(value: T | undefined, message: string): T {
|
export function assertDefined<T>(value: T | undefined, message: string): T {
|
||||||
if (value === null || typeof value === 'undefined') {
|
if (value === null || typeof value === 'undefined') {
|
||||||
throw new Error(message || 'Assertion failed');
|
throw new Error(message || 'Assertion failed');
|
||||||
|
78
core/src/debug/ChangeIndex.ts
Normal file
78
core/src/debug/ChangeIndex.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import Item from '../Item';
|
||||||
|
|
||||||
|
export default interface ChangeIndex {
|
||||||
|
/**
|
||||||
|
* Return the number of tracked changes.
|
||||||
|
*/
|
||||||
|
changeCount(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the change if for the given item if there is any
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
change(item: Item): Change | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returs true if the given item has changed
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
hasChanged(item: Item): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is a change and it's category is ChangeCategory.PLUS
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
isPlusChange(item: Item): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is a change and it's category is ChangeCategory.NEUTRAL
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
isNeutralChange(item: Item): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is a change and it's category is ChangeCategory.MINUS
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
isMinusChange(item: Item): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Change {
|
||||||
|
constructor(public category: ChangeCategory) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is merely for coloring different kind of changes
|
||||||
|
export enum ChangeCategory {
|
||||||
|
PLUS = 'PLUS',
|
||||||
|
MINUS = 'MINUS',
|
||||||
|
NEUTRAL = 'NEUTRAL',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Addition extends Change {
|
||||||
|
constructor() {
|
||||||
|
super(ChangeCategory.PLUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Removal extends Change {
|
||||||
|
constructor() {
|
||||||
|
super(ChangeCategory.MINUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContentChange extends Change {
|
||||||
|
constructor() {
|
||||||
|
super(ChangeCategory.NEUTRAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
UP = 'UP',
|
||||||
|
DOWN = 'DOWN',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PositionChange extends Change {
|
||||||
|
constructor(public direction: Direction, public amount: number) {
|
||||||
|
super(direction === Direction.UP ? ChangeCategory.PLUS : ChangeCategory.MINUS);
|
||||||
|
}
|
||||||
|
}
|
71
core/src/debug/ChangeTracker.ts
Normal file
71
core/src/debug/ChangeTracker.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import ChangeIndex, {
|
||||||
|
Change,
|
||||||
|
ChangeCategory,
|
||||||
|
Addition,
|
||||||
|
Removal,
|
||||||
|
PositionChange,
|
||||||
|
ContentChange,
|
||||||
|
Direction,
|
||||||
|
} from './ChangeIndex';
|
||||||
|
import Item from '../Item';
|
||||||
|
import { assertNot, assertDefined } from '../assert';
|
||||||
|
|
||||||
|
const ADDITION = new Addition();
|
||||||
|
const REMOVAL = new Removal();
|
||||||
|
const CONTENT_CHANGE = new ContentChange();
|
||||||
|
|
||||||
|
export default class ChangeTracker implements ChangeIndex {
|
||||||
|
private changes: Map<string, Change> = new Map();
|
||||||
|
|
||||||
|
private addChange(item: Item, change: Change) {
|
||||||
|
const uuid = _uuid(item);
|
||||||
|
assertNot(this.changes.has(uuid), `Change for item ${uuid} already defined`);
|
||||||
|
this.changes.set(uuid, change);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCount(): number {
|
||||||
|
return this.changes.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackAddition(item: Item) {
|
||||||
|
this.addChange(item, ADDITION);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackRemoval(item: Item) {
|
||||||
|
this.addChange(item, REMOVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackPositionalChange(item: Item, oldPosition: number, newPosition: number) {
|
||||||
|
const direction = newPosition > oldPosition ? Direction.DOWN : Direction.UP;
|
||||||
|
const amount = Math.abs(newPosition - oldPosition);
|
||||||
|
this.addChange(item, new PositionChange(direction, amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
trackContentChange(item: Item) {
|
||||||
|
this.addChange(item, CONTENT_CHANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
change(item: Item): Change | undefined {
|
||||||
|
return this.changes.get(_uuid(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChanged(item: Item): boolean {
|
||||||
|
return this.changes.has(_uuid(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlusChange(item: Item): boolean {
|
||||||
|
return this.change(item)?.category === ChangeCategory.PLUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNeutralChange(item: Item): boolean {
|
||||||
|
return this.change(item)?.category === ChangeCategory.NEUTRAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMinusChange(item: Item): boolean {
|
||||||
|
return this.change(item)?.category === ChangeCategory.MINUS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _uuid(item: Item): string {
|
||||||
|
return assertDefined(item.uuid, 'UUID is not set');
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import type Item from '../Item';
|
import type Item from '..//Item';
|
||||||
|
|
||||||
export default class ItemGroup {
|
export default class ItemGroup {
|
||||||
top: Item;
|
top: Item;
|
||||||
@ -12,4 +12,11 @@ export default class ItemGroup {
|
|||||||
hasMany(): boolean {
|
hasMany(): boolean {
|
||||||
return this.elements.length > 0;
|
return this.elements.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unpacked(): Item[] {
|
||||||
|
if (this.elements.length > 0) {
|
||||||
|
return this.elements;
|
||||||
|
}
|
||||||
|
return [this.top];
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import type ChangeTracker from './ChangeTracker';
|
||||||
import type Item from '../Item';
|
import type Item from '../Item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -5,5 +6,5 @@ import type Item from '../Item';
|
|||||||
*/
|
*/
|
||||||
export default abstract class ItemMerger {
|
export default abstract class ItemMerger {
|
||||||
constructor(public groupKey: string) {}
|
constructor(public groupKey: string) {}
|
||||||
abstract merge(items: Item[]): Item;
|
abstract merge(tracker: ChangeTracker, items: Item[]): Item;
|
||||||
}
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import ItemMerger from './ItemMerger';
|
import ItemMerger from './ItemMerger';
|
||||||
import Item from '../Item';
|
import Item from '../Item';
|
||||||
|
import ChangeTracker from './ChangeTracker';
|
||||||
|
|
||||||
export default class LineItemMerger extends ItemMerger {
|
export default class LineItemMerger extends ItemMerger {
|
||||||
constructor() {
|
constructor(private trackAsNew = false) {
|
||||||
super('line');
|
super('line');
|
||||||
}
|
}
|
||||||
|
|
||||||
merge(items: Item[]): Item {
|
merge(tracker: ChangeTracker, items: Item[]): Item {
|
||||||
const page = items[0].page;
|
const page = items[0].page;
|
||||||
const line = items[0].data['line'];
|
const line = items[0].data['line'];
|
||||||
const str = items.map((item) => item.data['str']).join(' ');
|
const str = items.map((item) => item.data['str']).join(' ');
|
||||||
@ -16,7 +17,7 @@ export default class LineItemMerger extends ItemMerger {
|
|||||||
const height = Math.max(...items.map((item) => item.data['height']));
|
const height = Math.max(...items.map((item) => item.data['height']));
|
||||||
const fontNames = [...new Set(items.map((item) => item.data['fontName']))];
|
const fontNames = [...new Set(items.map((item) => item.data['fontName']))];
|
||||||
const directions = [...new Set(items.map((item) => item.data['dir']))];
|
const directions = [...new Set(items.map((item) => item.data['dir']))];
|
||||||
return new Item(page, {
|
const newItem = new Item(page, {
|
||||||
str,
|
str,
|
||||||
line,
|
line,
|
||||||
x,
|
x,
|
||||||
@ -26,5 +27,12 @@ export default class LineItemMerger extends ItemMerger {
|
|||||||
fontName: fontNames,
|
fontName: fontNames,
|
||||||
dir: directions,
|
dir: directions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.trackAsNew) {
|
||||||
|
tracker.trackAddition(newItem);
|
||||||
|
} else if (items.find((item) => tracker.hasChanged(item))) {
|
||||||
|
tracker.trackContentChange(newItem);
|
||||||
|
}
|
||||||
|
return newItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
29
core/src/debug/Page.ts
Normal file
29
core/src/debug/Page.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import Item from '../Item';
|
||||||
|
import { groupByElement, groupByPage } from '../support/groupingUtils';
|
||||||
|
import ChangeTracker from './ChangeTracker';
|
||||||
|
import ItemGroup from './ItemGroup';
|
||||||
|
import ItemMerger from './ItemMerger';
|
||||||
|
|
||||||
|
export default interface Page {
|
||||||
|
index: number;
|
||||||
|
itemGroups: ItemGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asPages(tracker: ChangeTracker, items: Item[], itemMerger?: ItemMerger): Page[] {
|
||||||
|
return groupByPage(items).map((pageItems: Item[]) => {
|
||||||
|
let itemGroups: ItemGroup[];
|
||||||
|
if (itemMerger) {
|
||||||
|
itemGroups = groupByElement(pageItems, itemMerger.groupKey).map((groupItems) => {
|
||||||
|
if (groupItems.length > 1) {
|
||||||
|
const top = itemMerger.merge(tracker, groupItems);
|
||||||
|
return new ItemGroup(top, groupItems);
|
||||||
|
} else {
|
||||||
|
return new ItemGroup(groupItems[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
itemGroups = pageItems.map((item) => new ItemGroup(item));
|
||||||
|
}
|
||||||
|
return { index: pageItems[0].page, itemGroups } as Page;
|
||||||
|
});
|
||||||
|
}
|
@ -1,12 +1,35 @@
|
|||||||
import TransformDescriptor from '../TransformDescriptor';
|
import TransformDescriptor, { toDescriptor } from '../TransformDescriptor';
|
||||||
import Item from '../Item';
|
|
||||||
import AnnotatedColumn from './AnnotatedColumn';
|
import AnnotatedColumn from './AnnotatedColumn';
|
||||||
import { Change } from './detectChanges';
|
import Item from '../Item';
|
||||||
|
import Page, { asPages } from './Page';
|
||||||
|
import ChangeIndex from './ChangeIndex';
|
||||||
|
import ChangeTracker from './ChangeTracker';
|
||||||
|
|
||||||
export default interface StageResult {
|
export default class StageResult {
|
||||||
descriptor?: TransformDescriptor;
|
constructor(
|
||||||
schema: AnnotatedColumn[];
|
public descriptor: TransformDescriptor,
|
||||||
items: Item[];
|
public schema: AnnotatedColumn[],
|
||||||
changes: Map<string, Change>;
|
public pages: Page[],
|
||||||
messages: string[];
|
public changes: ChangeIndex,
|
||||||
|
public messages: string[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
itemsUnpacked(): Item[] {
|
||||||
|
return this.pages.reduce((items: Item[], page: Page) => {
|
||||||
|
page.itemGroups.forEach((itemGroup) => itemGroup.unpacked().forEach((item) => items.push(item)));
|
||||||
|
return items;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialStage(inputSchema: string[], inputItems: Item[]): StageResult {
|
||||||
|
const schema = inputSchema.map((column) => ({ name: column }));
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
|
const pages = asPages(tracker, inputItems);
|
||||||
|
const messages = [
|
||||||
|
`Parsed ${inputItems.length === 0 ? 0 : inputItems[inputItems.length - 1].page + 1} pages with ${
|
||||||
|
inputItems.length
|
||||||
|
} items`,
|
||||||
|
];
|
||||||
|
return new StageResult(toDescriptor({}), schema, pages, tracker, messages);
|
||||||
}
|
}
|
||||||
|
@ -1,67 +1,26 @@
|
|||||||
|
import ChangeTracker from './ChangeTracker';
|
||||||
import { assertDefined } from '../assert';
|
import { assertDefined } from '../assert';
|
||||||
import Item from '../Item';
|
import Item from '../Item';
|
||||||
|
|
||||||
// export enum ChangeType {
|
|
||||||
// POSITION_CHANGED = 'POSITION_CHANGED',
|
|
||||||
// ADDED = 'ADDED',
|
|
||||||
// REMOVED = 'REMOVED',
|
|
||||||
// CHANGED = 'CHANGED',
|
|
||||||
// EVALUATED = 'EVALUATED',
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Marker interface for a change
|
|
||||||
export abstract class Change {
|
|
||||||
constructor(public category: ChangeCategory) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is merely for coloring different kind of changes
|
|
||||||
export enum ChangeCategory {
|
|
||||||
PLUS = 'PLUS',
|
|
||||||
MINUS = 'MINUS',
|
|
||||||
NEUTRAL = 'NEUTRAL',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Addition extends Change {
|
|
||||||
constructor() {
|
|
||||||
super(ChangeCategory.PLUS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Direction {
|
|
||||||
UP = 'UP',
|
|
||||||
DOWN = 'DOWN',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PositionChange extends Change {
|
|
||||||
constructor(public direction: Direction, public amount: number) {
|
|
||||||
super(direction === Direction.UP ? ChangeCategory.PLUS : ChangeCategory.NEUTRAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares incomming and outgoing items of a transformer in order to detect changes and to display them in any debug visualization.
|
* Compares incomming and outgoing items of a transformer in order to detect changes and to display them in any debug visualization.
|
||||||
*/
|
*/
|
||||||
export function detectChanges(inputItems: Item[], transformedItems: Item[]): Map<string, Change> {
|
export function detectChanges(tracker: ChangeTracker, inputItems: Item[], transformedItems: Item[]) {
|
||||||
const changes: Map<string, Change> = new Map();
|
|
||||||
const inputItemsByUuid = inputMap(inputItems);
|
const inputItemsByUuid = inputMap(inputItems);
|
||||||
transformedItems.forEach((item, idx) => {
|
transformedItems.forEach((item, idx) => {
|
||||||
const uuid = getUuid(item);
|
const uuid = _uuid(item);
|
||||||
const oldItem = inputItemsByUuid.get(uuid);
|
const oldItem = inputItemsByUuid.get(uuid);
|
||||||
if (oldItem) {
|
if (oldItem) {
|
||||||
if (idx !== oldItem.position) {
|
if (idx !== oldItem.position) {
|
||||||
const direction = idx > oldItem.position ? Direction.DOWN : Direction.UP;
|
tracker.trackPositionalChange(item, oldItem.position, idx);
|
||||||
const amount = Math.abs(idx - oldItem.position);
|
|
||||||
changes.set(uuid, new PositionChange(direction, amount));
|
|
||||||
}
|
}
|
||||||
//TODO check for change
|
//TODO check for change
|
||||||
} else {
|
} else {
|
||||||
changes.set(uuid, new Addition());
|
tracker.trackAddition(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO detect removals (need to re-add them ?)
|
//TODO detect removals (need to re-add them ?)
|
||||||
|
|
||||||
return changes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputItem {
|
interface InputItem {
|
||||||
@ -71,11 +30,11 @@ interface InputItem {
|
|||||||
|
|
||||||
function inputMap(inputItems: Item[]): Map<string, InputItem> {
|
function inputMap(inputItems: Item[]): Map<string, InputItem> {
|
||||||
return inputItems.reduce((map, item, idx) => {
|
return inputItems.reduce((map, item, idx) => {
|
||||||
map.set(getUuid(item), { item, position: idx });
|
map.set(_uuid(item), { item, position: idx });
|
||||||
return map;
|
return map;
|
||||||
}, new Map());
|
}, new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUuid(item: Item): string {
|
function _uuid(item: Item): string {
|
||||||
return assertDefined(item.uuid, 'UUID is not set');
|
return assertDefined(item.uuid, 'UUID is not set');
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import type ItemGroup from './ItemGroup';
|
|
||||||
|
|
||||||
export default interface Page {
|
|
||||||
index: number;
|
|
||||||
itemGroups: ItemGroup[];
|
|
||||||
}
|
|
@ -1,7 +1,8 @@
|
|||||||
import ItemMerger from './ItemMerger';
|
import ItemMerger from '../debug/ItemMerger';
|
||||||
import Item from '../Item';
|
import Item from '../Item';
|
||||||
import ItemGroup from './ItemGroup';
|
import ItemGroup from '../debug/ItemGroup';
|
||||||
import Page from './Page';
|
import Page from '../debug/Page';
|
||||||
|
import ChangeTracker from '../debug/ChangeTracker';
|
||||||
|
|
||||||
type KeyExtractor = (item: Item) => any;
|
type KeyExtractor = (item: Item) => any;
|
||||||
type PageItemTransformer = (page: number, items: Item[]) => Item[];
|
type PageItemTransformer = (page: number, items: Item[]) => Item[];
|
||||||
@ -42,22 +43,3 @@ export function transformGroupedByPageAndLine(items: Item[], groupedTransformer:
|
|||||||
});
|
});
|
||||||
return transformedItems;
|
return transformedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asPages(items: Item[], itemMerger?: ItemMerger): Page[] {
|
|
||||||
return groupByPage(items).map((pageItems: Item[]) => {
|
|
||||||
let itemGroups: ItemGroup[];
|
|
||||||
if (itemMerger) {
|
|
||||||
itemGroups = groupByElement(pageItems, itemMerger.groupKey).map((groupItems) => {
|
|
||||||
if (groupItems.length > 1) {
|
|
||||||
const top = itemMerger.merge(groupItems);
|
|
||||||
return new ItemGroup(top, groupItems);
|
|
||||||
} else {
|
|
||||||
return new ItemGroup(groupItems[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
itemGroups = pageItems.map((item) => new ItemGroup(item));
|
|
||||||
}
|
|
||||||
return { index: pageItems[0].page, itemGroups } as Page;
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,9 +1,8 @@
|
|||||||
import PageViewport from '../parse/PageViewport';
|
|
||||||
import Item from '../Item';
|
import Item from '../Item';
|
||||||
import ItemResult from '../ItemResult';
|
import ItemResult from '../ItemResult';
|
||||||
import ItemTransformer from './ItemTransformer';
|
import ItemTransformer from './ItemTransformer';
|
||||||
import TransformContext from './TransformContext';
|
import TransformContext from './TransformContext';
|
||||||
import { transformGroupedByPage } from '../support/itemUtils';
|
import { transformGroupedByPage } from '../support/groupingUtils';
|
||||||
|
|
||||||
export default class AdjustHeight extends ItemTransformer {
|
export default class AdjustHeight extends ItemTransformer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -2,8 +2,8 @@ import Item from '../Item';
|
|||||||
import ItemResult from '../ItemResult';
|
import ItemResult from '../ItemResult';
|
||||||
import ItemTransformer from './ItemTransformer';
|
import ItemTransformer from './ItemTransformer';
|
||||||
import TransformContext from './TransformContext';
|
import TransformContext from './TransformContext';
|
||||||
import { transformGroupedByPage } from '../support/itemUtils';
|
import LineItemMerger from '../debug/LineItemMerger';
|
||||||
import LineItemMerger from '../support/LineItemMerger';
|
import { transformGroupedByPage } from '../support/groupingUtils';
|
||||||
|
|
||||||
export default class CompactLines extends ItemTransformer {
|
export default class CompactLines extends ItemTransformer {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -13,7 +13,7 @@ export default class CompactLines extends ItemTransformer {
|
|||||||
{
|
{
|
||||||
requireColumns: ['str', 'y', 'height'],
|
requireColumns: ['str', 'y', 'height'],
|
||||||
debug: {
|
debug: {
|
||||||
itemMerger: new LineItemMerger(),
|
itemMerger: new LineItemMerger(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
(incomingSchema) => {
|
(incomingSchema) => {
|
||||||
|
@ -2,8 +2,8 @@ import Item from '../Item';
|
|||||||
import ItemResult from '../ItemResult';
|
import ItemResult from '../ItemResult';
|
||||||
import ItemTransformer from './ItemTransformer';
|
import ItemTransformer from './ItemTransformer';
|
||||||
import TransformContext from './TransformContext';
|
import TransformContext from './TransformContext';
|
||||||
import LineItemMerger from '../support/LineItemMerger';
|
import LineItemMerger from '../debug/LineItemMerger';
|
||||||
import { transformGroupedByPageAndLine } from '../support/itemUtils';
|
import { transformGroupedByPageAndLine } from '../support/groupingUtils';
|
||||||
|
|
||||||
export default class SortXWithinLines extends ItemTransformer {
|
export default class SortXWithinLines extends ItemTransformer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -3,17 +3,19 @@ import Item from 'src/Item';
|
|||||||
import ItemTransformer from 'src/transformer/ItemTransformer';
|
import ItemTransformer from 'src/transformer/ItemTransformer';
|
||||||
import TransformDescriptor from 'src/TransformDescriptor';
|
import TransformDescriptor from 'src/TransformDescriptor';
|
||||||
import TransformContext from 'src/transformer/TransformContext';
|
import TransformContext from 'src/transformer/TransformContext';
|
||||||
|
import LineItemMerger from 'src/debug/LineItemMerger';
|
||||||
import ItemResult from 'src/ItemResult';
|
import ItemResult from 'src/ItemResult';
|
||||||
import ColumnAnnotation from 'src/debug/ColumnAnnotation';
|
import ColumnAnnotation from 'src/debug/ColumnAnnotation';
|
||||||
import AnnotatedColumn from 'src/debug/AnnotatedColumn';
|
import AnnotatedColumn from 'src/debug/AnnotatedColumn';
|
||||||
|
import { items } from './testItems';
|
||||||
|
|
||||||
class TestTransformer extends ItemTransformer {
|
class TestTransformer extends ItemTransformer {
|
||||||
items: Item[];
|
items: Item[];
|
||||||
constructor(name: string, descriptor: Partial<TransformDescriptor>, outputSchema: string[], items: Item[]) {
|
constructor(name: string, descriptor: Partial<TransformDescriptor>, outputSchema: string[], items: Item[]) {
|
||||||
super(name, `Description for ${name}`, descriptor, (incomingSchema) => outputSchema);
|
super(name, `Description for ${name}`, descriptor, (_) => outputSchema);
|
||||||
this.items = items;
|
this.items = items;
|
||||||
}
|
}
|
||||||
transform(_: TransformContext, items: Item[]): ItemResult {
|
transform(_: TransformContext, __: Item[]): ItemResult {
|
||||||
return {
|
return {
|
||||||
items: this.items,
|
items: this.items,
|
||||||
messages: [],
|
messages: [],
|
||||||
@ -21,9 +23,13 @@ class TestTransformer extends ItemTransformer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('basic debug', async () => {
|
describe('Transform Items', () => {
|
||||||
|
test('Basics', async () => {
|
||||||
const parsedSchema = ['A', 'B'];
|
const parsedSchema = ['A', 'B'];
|
||||||
const parsedItems = [new Item(0, { A: 'a_row1', B: 'b_row1' }), new Item(0, { A: 'a_row2', B: 'b_row2' })];
|
const parsedItems = items(0, [
|
||||||
|
{ A: 'a_row1', B: 'b_row1' },
|
||||||
|
{ A: 'a_row2', B: 'b_row2' },
|
||||||
|
]);
|
||||||
|
|
||||||
const trans1Desc = { requireColumns: ['A', 'B'] };
|
const trans1Desc = { requireColumns: ['A', 'B'] };
|
||||||
const trans1Schema = ['C'];
|
const trans1Schema = ['C'];
|
||||||
@ -38,10 +44,91 @@ test('basic debug', async () => {
|
|||||||
...parsedSchema.map((column) => ({ name: column, annotation: ColumnAnnotation.REMOVED })),
|
...parsedSchema.map((column) => ({ name: column, annotation: ColumnAnnotation.REMOVED })),
|
||||||
{ name: 'C', annotation: ColumnAnnotation.ADDED },
|
{ name: 'C', annotation: ColumnAnnotation.ADDED },
|
||||||
]);
|
]);
|
||||||
expect(debug.stageResults(0).items).toEqual(parsedItems);
|
|
||||||
expect(debug.stageResults(1).items).toEqual(trans1Items);
|
expect(debug.stageResults(0).itemsUnpacked()).toEqual(parsedItems);
|
||||||
|
expect(debug.stageResults(1).itemsUnpacked()).toEqual(trans1Items);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Line Merge', async () => {
|
||||||
|
const parsedSchema = ['id', 'y'];
|
||||||
|
const parsedItems = items(0, [
|
||||||
|
{ id: 1, y: 1 },
|
||||||
|
{ id: 2, y: 2 },
|
||||||
|
{ id: 3, y: 2 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const trans1Desc = { requireColumns: ['id', 'y'], debug: { itemMerger: new LineItemMerger(true) } };
|
||||||
|
const trans1Schema = ['id', 'line'];
|
||||||
|
const trans1Items = parsedItems.map((item) => item.withData({ line: item.data['y'] }));
|
||||||
|
|
||||||
|
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
||||||
|
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
||||||
|
|
||||||
|
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
||||||
|
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'y' }]);
|
||||||
|
expect(debug.stageResults(1).schema).toEqual([
|
||||||
|
{ name: 'id' },
|
||||||
|
{ name: 'y', annotation: ColumnAnnotation.REMOVED },
|
||||||
|
{ name: 'line', annotation: ColumnAnnotation.ADDED },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(debug.stageResults(0).itemsUnpacked()).toEqual(parsedItems);
|
||||||
|
expect(debug.stageResults(1).itemsUnpacked()).toEqual(trans1Items);
|
||||||
|
|
||||||
|
const lineMergingStage = debug.stageResults(1);
|
||||||
|
const { changes, pages } = lineMergingStage;
|
||||||
|
|
||||||
|
//verify item groups
|
||||||
|
expect(pages[0].itemGroups.map((itemGroup) => changes.hasChanged(itemGroup.top))).toEqual([false, true]);
|
||||||
|
|
||||||
|
//verify unpacked items
|
||||||
|
expect(lineMergingStage.itemsUnpacked().map((item) => changes.hasChanged(item))).toEqual([false, false, false]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change inside of Line', async () => {
|
||||||
|
const parsedSchema = ['id', 'line'];
|
||||||
|
const parsedItems = items(0, [
|
||||||
|
{ id: 1, line: 1 },
|
||||||
|
{ id: 2, line: 1 },
|
||||||
|
{ id: 3, line: 2 },
|
||||||
|
{ id: 4, line: 2 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const trans1Desc = { requireColumns: ['id', 'line'], debug: { itemMerger: new LineItemMerger() } };
|
||||||
|
const trans1Schema = parsedSchema;
|
||||||
|
const trans1Items = swapElements([...parsedItems], 0, 1);
|
||||||
|
|
||||||
|
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
|
||||||
|
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
|
||||||
|
|
||||||
|
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
|
||||||
|
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'line' }]);
|
||||||
|
expect(debug.stageResults(1).schema).toEqual([{ name: 'id' }, { name: 'line' }]);
|
||||||
|
expect(debug.stageResults(0).itemsUnpacked()).toEqual(parsedItems);
|
||||||
|
expect(debug.stageResults(1).itemsUnpacked()).toEqual(trans1Items);
|
||||||
|
|
||||||
|
const { changes, pages } = debug.stageResults(1);
|
||||||
|
|
||||||
|
//verify item groups
|
||||||
|
expect(pages[0].itemGroups.map((itemGroup) => changes.hasChanged(itemGroup.top))).toEqual([true, false]);
|
||||||
|
|
||||||
|
//verify unpacked items
|
||||||
|
expect(
|
||||||
|
debug
|
||||||
|
.stageResults(1)
|
||||||
|
.itemsUnpacked()
|
||||||
|
.map((item) => changes.hasChanged(item)),
|
||||||
|
).toEqual([true, true, false, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
var swapElements = function (arr: Item[], indexA: number, indexB: number): Item[] {
|
||||||
|
var temp = arr[indexA];
|
||||||
|
arr[indexA] = arr[indexB];
|
||||||
|
arr[indexB] = temp;
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
describe('build schemas', () => {
|
describe('build schemas', () => {
|
||||||
const items: Item[] = [];
|
const items: Item[] = [];
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import LineItemMerger from 'src/debug/LineItemMerger';
|
import LineItemMerger from 'src/debug/LineItemMerger';
|
||||||
|
import ChangeTracker from 'src/debug/ChangeTracker';
|
||||||
import Item from 'src/Item';
|
import Item from 'src/Item';
|
||||||
import { items } from '../testItems';
|
import { items, realisticItems } from '../testItems';
|
||||||
|
import { Addition, ContentChange } from 'src/debug/ChangeIndex';
|
||||||
const itemMerger = new LineItemMerger();
|
|
||||||
|
|
||||||
test('Basics', async () => {
|
test('Basics', async () => {
|
||||||
|
const itemMerger = new LineItemMerger();
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
expect(itemMerger.groupKey).toEqual('line');
|
expect(itemMerger.groupKey).toEqual('line');
|
||||||
|
const mergedItem = itemMerger.merge(
|
||||||
const mergedItem = itemMerger?.merge(
|
tracker,
|
||||||
items(0, [
|
items(0, [
|
||||||
{
|
{
|
||||||
line: 2,
|
line: 2,
|
||||||
@ -41,7 +43,7 @@ test('Basics', async () => {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
expect(mergedItem?.withoutUuid()).toEqual(
|
expect(mergedItem.withoutUuid()).toEqual(
|
||||||
new Item(0, {
|
new Item(0, {
|
||||||
line: 2,
|
line: 2,
|
||||||
x: 240,
|
x: 240,
|
||||||
@ -54,3 +56,22 @@ test('Basics', async () => {
|
|||||||
}).withoutUuid(),
|
}).withoutUuid(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Track all lines as changes', async () => {
|
||||||
|
const itemMerger = new LineItemMerger(true);
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
|
const mergedItem = itemMerger.merge(tracker, realisticItems(0, [{ line: 1 }, { line: 1 }]));
|
||||||
|
expect(tracker.change(mergedItem)).toEqual(new Addition());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mark lines containing changed items as changed', async () => {
|
||||||
|
const itemMerger = new LineItemMerger();
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
|
const items1 = realisticItems(0, [{ line: 1 }, { line: 1 }]);
|
||||||
|
const items2 = realisticItems(0, [{ line: 2 }, { line: 2 }]);
|
||||||
|
tracker.trackPositionalChange(items1[1], 1, 0);
|
||||||
|
const mergedItem1 = itemMerger.merge(tracker, items1);
|
||||||
|
const mergedItem2 = itemMerger.merge(tracker, items2);
|
||||||
|
expect(tracker.change(mergedItem1)).toEqual(new ContentChange());
|
||||||
|
expect(tracker.change(mergedItem2)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
60
core/test/debug/Page.test.ts
Normal file
60
core/test/debug/Page.test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import Item from 'src/Item';
|
||||||
|
import Page, { asPages } from 'src/debug/Page';
|
||||||
|
import ItemGroup from 'src/debug/ItemGroup';
|
||||||
|
import ItemMerger from 'src/debug/ItemMerger';
|
||||||
|
import { items } from 'test/testItems';
|
||||||
|
import ChangeTracker from 'src/debug/ChangeTracker';
|
||||||
|
|
||||||
|
test('empty', async () => {
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
|
expect(asPages(tracker, [])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no merger', async () => {
|
||||||
|
const pageItems = [
|
||||||
|
items(0, [{ id: 1, line: 1 }]),
|
||||||
|
items(1, [
|
||||||
|
{ id: 2, line: 1 },
|
||||||
|
{ id: 3, line: 1 },
|
||||||
|
{ id: 4, line: 1 },
|
||||||
|
]),
|
||||||
|
items(2, [{ id: 5, line: 1 }]),
|
||||||
|
];
|
||||||
|
const flattenedItems = new Array<Item>().concat(...pageItems);
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
|
const pages = asPages(tracker, flattenedItems);
|
||||||
|
expect(pages).toEqual([
|
||||||
|
{ index: 0, itemGroups: pageItems[0].map((item) => new ItemGroup(item)) },
|
||||||
|
{ index: 1, itemGroups: pageItems[1].map((item) => new ItemGroup(item)) },
|
||||||
|
{ index: 2, itemGroups: pageItems[2].map((item) => new ItemGroup(item)) },
|
||||||
|
] as Page[]);
|
||||||
|
expect(tracker.changeCount()).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('merger', async () => {
|
||||||
|
const pageItems = [
|
||||||
|
items(0, [{ id: 1, line: 1 }]),
|
||||||
|
items(1, [
|
||||||
|
{ id: 2, line: 1 },
|
||||||
|
{ id: 3, line: 1 },
|
||||||
|
{ id: 4, line: 2 },
|
||||||
|
]),
|
||||||
|
items(2, [{ id: 5, line: 1 }]),
|
||||||
|
];
|
||||||
|
const flattenedItems = new Array<Item>().concat(...pageItems);
|
||||||
|
const merger: ItemMerger = { groupKey: 'line', merge: (items) => items[0] };
|
||||||
|
const tracker = new ChangeTracker();
|
||||||
|
const pages = asPages(tracker, flattenedItems, merger);
|
||||||
|
|
||||||
|
expect(pages).toEqual([
|
||||||
|
{ index: 0, itemGroups: pageItems[0].map((item) => new ItemGroup(item)) },
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
itemGroups: [
|
||||||
|
new ItemGroup(merger.merge(tracker, pageItems[1].slice(0, 2)), pageItems[1].slice(0, 2)),
|
||||||
|
new ItemGroup(pageItems[1][2]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ index: 2, itemGroups: pageItems[2].map((item) => new ItemGroup(item)) },
|
||||||
|
] as Page[]);
|
||||||
|
});
|
@ -1,4 +1,6 @@
|
|||||||
import { detectChanges, PositionChange, Direction } from 'src/debug/detectChanges';
|
import ChangeTracker from 'src/debug/ChangeTracker';
|
||||||
|
import { detectChanges } from 'src/debug/detectChanges';
|
||||||
|
import { PositionChange, Direction } from 'src/debug/ChangeIndex';
|
||||||
import { items } from 'test/testItems';
|
import { items } from 'test/testItems';
|
||||||
|
|
||||||
test('No changes', async () => {
|
test('No changes', async () => {
|
||||||
@ -9,8 +11,9 @@ test('No changes', async () => {
|
|||||||
{ id: 'B', line: '1', x: 2 },
|
{ id: 'B', line: '1', x: 2 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const changes = detectChanges(inputItems, inputItems);
|
const tracker = new ChangeTracker();
|
||||||
expect(changes).toEqual(new Map());
|
detectChanges(tracker, inputItems, inputItems);
|
||||||
|
expect(tracker.changeCount()).toEqual(0);
|
||||||
});
|
});
|
||||||
test('Positional changes', async () => {
|
test('Positional changes', async () => {
|
||||||
const inputItems = items(0, [
|
const inputItems = items(0, [
|
||||||
@ -22,11 +25,9 @@ test('Positional changes', async () => {
|
|||||||
|
|
||||||
const transformedItems = [inputItems[0], inputItems[3], inputItems[2], inputItems[1]];
|
const transformedItems = [inputItems[0], inputItems[3], inputItems[2], inputItems[1]];
|
||||||
|
|
||||||
const changes = detectChanges(inputItems, transformedItems);
|
const tracker = new ChangeTracker();
|
||||||
expect(changes).toEqual(
|
const changes = detectChanges(tracker, inputItems, transformedItems);
|
||||||
new Map([
|
expect(tracker.changeCount()).toEqual(2);
|
||||||
[inputItems[1].uuid, new PositionChange(Direction.DOWN, 2)],
|
expect(tracker.change(inputItems[1])).toEqual(new PositionChange(Direction.DOWN, 2));
|
||||||
[inputItems[3].uuid, new PositionChange(Direction.UP, 2)],
|
expect(tracker.change(inputItems[3])).toEqual(new PositionChange(Direction.UP, 2));
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import Item from 'src/Item';
|
import Item from 'src/Item';
|
||||||
import Page from 'src/support/Page';
|
|
||||||
import {
|
import {
|
||||||
groupByPage,
|
groupByPage,
|
||||||
groupByElement,
|
groupByElement,
|
||||||
transformGroupedByPage,
|
transformGroupedByPage,
|
||||||
transformGroupedByPageAndLine,
|
transformGroupedByPageAndLine,
|
||||||
asPages,
|
} from 'src/support/groupingUtils';
|
||||||
} from 'src/support/itemUtils';
|
|
||||||
import ItemGroup from 'src/support/ItemGroup';
|
|
||||||
import ItemMerger from 'src/support/ItemMerger';
|
|
||||||
import { items } from 'test/testItems';
|
import { items } from 'test/testItems';
|
||||||
|
|
||||||
describe('groupByPage', () => {
|
describe('groupByPage', () => {
|
||||||
@ -97,46 +93,3 @@ describe('transformGroupedByPageAndLine', () => {
|
|||||||
expect(transformedItems.map((item) => item.data['group'])).toEqual(['0/1:1', '1/1:2', '1/2:1', '2/1:1']);
|
expect(transformedItems.map((item) => item.data['group'])).toEqual(['0/1:1', '1/1:2', '1/2:1', '2/1:1']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('asPages', () => {
|
|
||||||
test('empty', async () => {
|
|
||||||
expect(groupByPage([])).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('no merger', async () => {
|
|
||||||
const pageItems = [
|
|
||||||
[new Item(0, { id: 1, line: 1 })],
|
|
||||||
[new Item(1, { id: 2, line: 1 }), new Item(1, { id: 3, line: 1 }), new Item(1, { id: 4, line: 2 })],
|
|
||||||
[new Item(2, { id: 5, line: 1 })],
|
|
||||||
];
|
|
||||||
const flattenedItems = new Array<Item>().concat(...pageItems);
|
|
||||||
const pages = asPages(flattenedItems);
|
|
||||||
expect(pages).toEqual([
|
|
||||||
{ index: 0, itemGroups: pageItems[0].map((item) => new ItemGroup(item)) },
|
|
||||||
{ index: 1, itemGroups: pageItems[1].map((item) => new ItemGroup(item)) },
|
|
||||||
{ index: 2, itemGroups: pageItems[2].map((item) => new ItemGroup(item)) },
|
|
||||||
] as Page[]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('merger', async () => {
|
|
||||||
const pageItems = [
|
|
||||||
[new Item(0, { id: 1, line: 1 })],
|
|
||||||
[new Item(1, { id: 2, line: 1 }), new Item(1, { id: 3, line: 1 }), new Item(1, { id: 4, line: 2 })],
|
|
||||||
[new Item(2, { id: 5, line: 1 })],
|
|
||||||
];
|
|
||||||
const flattenedItems = new Array<Item>().concat(...pageItems);
|
|
||||||
const merger: ItemMerger = { groupKey: 'line', merge: (items) => items[0] };
|
|
||||||
const pages = asPages(flattenedItems, merger);
|
|
||||||
expect(pages).toEqual([
|
|
||||||
{ index: 0, itemGroups: pageItems[0].map((item) => new ItemGroup(item)) },
|
|
||||||
{
|
|
||||||
index: 1,
|
|
||||||
itemGroups: [
|
|
||||||
new ItemGroup(merger.merge(pageItems[1].slice(0, 2)), pageItems[1].slice(0, 2)),
|
|
||||||
new ItemGroup(pageItems[1][2]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ index: 2, itemGroups: pageItems[2].map((item) => new ItemGroup(item)) },
|
|
||||||
] as Page[]);
|
|
||||||
});
|
|
||||||
});
|
|
@ -3,3 +3,20 @@ import Item from 'src/Item';
|
|||||||
export function items(page: number, data: object[]): Item[] {
|
export function items(page: number, data: object[]): Item[] {
|
||||||
return data.map((data) => new Item(page, data));
|
return data.map((data) => new Item(page, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function realisticItems(page: number, data: object[]): Item[] {
|
||||||
|
return data.map(
|
||||||
|
(data, idx) =>
|
||||||
|
new Item(page, {
|
||||||
|
line: idx,
|
||||||
|
x: idx,
|
||||||
|
y: idx,
|
||||||
|
str: `text ${idx}`,
|
||||||
|
fontName: 'g_d0_f2',
|
||||||
|
dir: 'ltr',
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
...data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
5
ui/src/components/ComponentDefinition.ts
Normal file
5
ui/src/components/ComponentDefinition.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { SvelteComponent } from 'svelte';
|
||||||
|
|
||||||
|
export default class ComponentDefinition {
|
||||||
|
constructor(public component: object, public args: object = {}) {}
|
||||||
|
}
|
@ -1,25 +1,40 @@
|
|||||||
<script>
|
<script>
|
||||||
import type { Change } from '@core/debug/detectChanges';
|
import type ChangeIndex from '@core/debug/ChangeIndex';
|
||||||
import { PositionChange, Direction } from '../../../core/src/debug/detectChanges';
|
import type Item from '@core/Item';
|
||||||
|
import { Addition, ContentChange, PositionChange, Direction } from '../../../core/src/debug/ChangeIndex';
|
||||||
|
import ComponentDefinition from '../components/ComponentDefinition';
|
||||||
|
|
||||||
import Icon from 'fa-svelte';
|
import {
|
||||||
import type { IconDefinition } from '@fortawesome/fontawesome-common-types/index';
|
PlusCircle as Plus,
|
||||||
import { faArrowUp as up } from '@fortawesome/free-solid-svg-icons/faArrowUp';
|
Adjustments as Changed,
|
||||||
import { faArrowDown as down } from '@fortawesome/free-solid-svg-icons/faArrowDown';
|
ArrowCircleUp as Up,
|
||||||
|
ArrowCircleDown as Down,
|
||||||
|
} from 'svelte-hero-icons';
|
||||||
|
|
||||||
export let changes: Map<string, Change>;
|
export let changes: ChangeIndex;
|
||||||
export let itemUid: string;
|
export let item: Item;
|
||||||
|
|
||||||
|
$: hasChanged = changes.hasChanged(item);
|
||||||
let changeContent: string;
|
let changeContent: string;
|
||||||
let icon: IconDefinition;
|
let iconComp: ComponentDefinition;
|
||||||
$: {
|
$: {
|
||||||
const change = changes.get(itemUid);
|
if (hasChanged) {
|
||||||
if (change) {
|
let args = { size: '14' };
|
||||||
|
let change = changes.change(item);
|
||||||
switch (change.constructor.name) {
|
switch (change.constructor.name) {
|
||||||
case PositionChange.name:
|
case PositionChange.name:
|
||||||
const positionChange = change as PositionChange;
|
const positionChange = change as PositionChange;
|
||||||
changeContent = `${positionChange.amount}`;
|
changeContent = `${positionChange.amount}`;
|
||||||
icon = positionChange.direction === Direction.UP ? up : down;
|
iconComp =
|
||||||
|
positionChange.direction === Direction.UP
|
||||||
|
? new ComponentDefinition(Up, args)
|
||||||
|
: new ComponentDefinition(Down, args);
|
||||||
|
break;
|
||||||
|
case Addition.name:
|
||||||
|
iconComp = new ComponentDefinition(Plus, args);
|
||||||
|
break;
|
||||||
|
case ContentChange.name:
|
||||||
|
iconComp = new ComponentDefinition(Changed, args);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`${change.constructor.name}: ${change}`);
|
throw new Error(`${change.constructor.name}: ${change}`);
|
||||||
@ -28,11 +43,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if changeContent}
|
{#if hasChanged}
|
||||||
<div class="flex space-x-0.5 items-center text-xs">
|
<div class="flex space-x-0.5 items-center text-xs">
|
||||||
{#if icon}
|
{#if iconComp}
|
||||||
<Icon {icon} />
|
<svelte:component this={iconComp.component} {...iconComp.args} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if changeContent}
|
||||||
<div>{changeContent}</div>
|
<div>{changeContent}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
|
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
|
||||||
|
|
||||||
import type Debugger from '@core/Debugger';
|
import type Debugger from '@core/Debugger';
|
||||||
import { asPages } from '../../../core/src/support/itemUtils';
|
|
||||||
|
|
||||||
import Popup from '../components/Popup.svelte';
|
import Popup from '../components/Popup.svelte';
|
||||||
import PageSelectionPopup from './PageSelectionPopup.svelte';
|
import PageSelectionPopup from './PageSelectionPopup.svelte';
|
||||||
@ -24,10 +23,9 @@
|
|||||||
$: canPrev = currentStage > 0;
|
$: canPrev = currentStage > 0;
|
||||||
$: stageResult = debug.stageResults(currentStage);
|
$: stageResult = debug.stageResults(currentStage);
|
||||||
$: pageIsPinned = !isNaN(pinnedPage);
|
$: pageIsPinned = !isNaN(pinnedPage);
|
||||||
$: pagesNumbers = new Set(stageResult.items.map((item) => item.page));
|
$: pagesNumbers = new Set(stageResult.pages.map((page) => page.index));
|
||||||
$: pages = asPages(stageResult.items, stageResult.descriptor?.debug?.itemMerger);
|
|
||||||
$: maxPage = Math.max(...pagesNumbers);
|
$: maxPage = Math.max(...pagesNumbers);
|
||||||
$: visiblePages = pageIsPinned ? pages.filter((page) => page.index === pinnedPage) : pages;
|
$: visiblePages = pageIsPinned ? stageResult.pages.filter((page) => page.index === pinnedPage) : stageResult.pages;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-4">
|
<div class="mx-4">
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
import { scale, fade } from 'svelte/transition';
|
import { scale, fade } from 'svelte/transition';
|
||||||
import type AnnotatedColumn from '@core/debug/AnnotatedColumn';
|
import type AnnotatedColumn from '@core/debug/AnnotatedColumn';
|
||||||
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
|
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
|
||||||
import type { Change } from '@core/debug/detectChanges';
|
import type ChangeIndex from '@core/debug/ChangeIndex';
|
||||||
import { ChangeCategory } from '../../../core/src/debug/detectChanges';
|
|
||||||
import inView from '../actions/inView';
|
import inView from '../actions/inView';
|
||||||
import { formatValue } from './formatValues';
|
import { formatValue } from './formatValues';
|
||||||
import type Page from '@core/support/Page';
|
import type Page from '@core/debug/Page';
|
||||||
import ChangeSymbol from './ChangeSymbol.svelte';
|
import ChangeSymbol from './ChangeSymbol.svelte';
|
||||||
|
|
||||||
export let schema: AnnotatedColumn[];
|
export let schema: AnnotatedColumn[];
|
||||||
@ -14,7 +13,7 @@
|
|||||||
export let maxPage: number;
|
export let maxPage: number;
|
||||||
export let pageIsPinned: boolean;
|
export let pageIsPinned: boolean;
|
||||||
export let onlyRelevantItems: boolean;
|
export let onlyRelevantItems: boolean;
|
||||||
export let changes: Map<string, Change>;
|
export let changes: ChangeIndex;
|
||||||
let maxItemsToRenderInOneLoad = 200;
|
let maxItemsToRenderInOneLoad = 200;
|
||||||
let renderedMaxPage = 0;
|
let renderedMaxPage = 0;
|
||||||
let expandedItemGroup: { pageIndex: number; itemIndex: number };
|
let expandedItemGroup: { pageIndex: number; itemIndex: number };
|
||||||
@ -75,17 +74,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Page items -->
|
<!-- Page items -->
|
||||||
{#each page.itemGroups.filter((group) => !onlyRelevantItems || changes.has(group.top.uuid)) as itemGroup, itemIdx}
|
{#each page.itemGroups.filter((group) => !onlyRelevantItems || changes.hasChanged(group.top)) as itemGroup, itemIdx}
|
||||||
<tr
|
<tr
|
||||||
class:expandable={itemGroup.hasMany()}
|
class:expandable={itemGroup.hasMany()}
|
||||||
class:expanded={expandedItemGroup && isExpanded(page.index, itemIdx)}
|
class:expanded={expandedItemGroup && isExpanded(page.index, itemIdx)}
|
||||||
class:changePlus={changes.get(itemGroup.top.uuid)?.category === ChangeCategory.PLUS}
|
class:changePlus={changes.isPlusChange(itemGroup.top)}
|
||||||
class:changeNeutral={changes.get(itemGroup.top.uuid)?.category === ChangeCategory.NEUTRAL}
|
class:changeNeutral={changes.isNeutralChange(itemGroup.top)}
|
||||||
class:changeMinus={changes.get(itemGroup.top.uuid)?.category === ChangeCategory.MINUS}
|
class:changeMinus={changes.isMinusChange(itemGroup.top)}
|
||||||
in:fade>
|
in:fade>
|
||||||
<!-- 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">
|
<td id="page" class="page bg-gray-50 align-top">
|
||||||
<div>Page {page.index} {pageIsPinned ? '' : ' / ' + maxPage}</div>
|
<div>Page {page.index} {pageIsPinned ? '' : ' / ' + maxPage}</div>
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
@ -93,9 +92,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<span class="contents" on:click={() => itemGroup.hasMany() && toggleRow(page.index, itemIdx)}>
|
<span class="contents" on:click={() => itemGroup.hasMany() && toggleRow(page.index, itemIdx)}>
|
||||||
<!-- ID & change marker column -->
|
<!-- ID & change marker column -->
|
||||||
<td class="flex space-x-1">
|
<td>
|
||||||
<div>{itemIdx}{itemGroup.hasMany() ? '+' : ''}</div>
|
<div class="flex space-x-0.5 items-center">
|
||||||
<ChangeSymbol {changes} itemUid={itemGroup.top.uuid} />
|
<ChangeSymbol {changes} item={itemGroup.top} />
|
||||||
|
<div>{itemIdx}{itemGroup.hasMany() ? '…' : ''}</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Row values -->
|
<!-- Row values -->
|
||||||
@ -108,9 +109,18 @@
|
|||||||
<!-- Expanded childs -->
|
<!-- Expanded childs -->
|
||||||
{#if expandedItemGroup && isExpanded(page.index, itemIdx)}
|
{#if expandedItemGroup && isExpanded(page.index, itemIdx)}
|
||||||
{#each itemGroup.elements as child, childIdx}
|
{#each itemGroup.elements as child, childIdx}
|
||||||
<tr class="childs">
|
<tr
|
||||||
|
class="childs"
|
||||||
|
class:changePlus={changes.isPlusChange(child)}
|
||||||
|
class:changeNeutral={changes.isNeutralChange(child)}
|
||||||
|
class:changeMinus={changes.isMinusChange(child)}>
|
||||||
<td id="page" />
|
<td id="page" />
|
||||||
<td class="whitespace-nowrap">{'└ ' + childIdx}</td>
|
<td class="whitespace-nowrap">
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<div class="w-8">{'└ ' + childIdx}</div>
|
||||||
|
<ChangeSymbol {changes} item={child} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
{#each schema as column}
|
{#each schema as column}
|
||||||
<td class="select-all">{formatValue(child.data[column.name])}</td>
|
<td class="select-all">{formatValue(child.data[column.name])}</td>
|
||||||
{/each}
|
{/each}
|
||||||
@ -122,7 +132,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{#if onlyRelevantItems && changes.size === 0}
|
{#if onlyRelevantItems && changes.changeCount() === 0}
|
||||||
<div class="flex space-x-1 items-center justify-center text-xl">
|
<div class="flex space-x-1 items-center justify-center text-xl">
|
||||||
<div>No changes from the transformation.</div>
|
<div>No changes from the transformation.</div>
|
||||||
<div>Want to see</div>
|
<div>Want to see</div>
|
||||||
@ -144,13 +154,12 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page {
|
.page {
|
||||||
@apply text-lg;
|
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply pr-4;
|
@apply pr-4;
|
||||||
@apply whitespace-nowrap;
|
@apply whitespace-nowrap;
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 2em;
|
top: 2.4em;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +167,7 @@
|
|||||||
@apply px-1;
|
@apply px-1;
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 2.4em;
|
top: 2.6em;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
th:not(:first-child) {
|
th:not(:first-child) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user