Remove UI package

This commit is contained in:
Johannes Zillmann 2024-03-19 16:50:38 -06:00
parent 02c2fd04fe
commit e56d70c599
43 changed files with 0 additions and 11254 deletions

4
ui/.gitignore vendored
View File

@ -1,4 +0,0 @@
.build
build
web_modules
node_modules

View File

@ -1,2 +0,0 @@
package-lock.json
public/build/

View File

@ -1,8 +0,0 @@
{
"svelteSortOrder": "scripts-markup-styles",
"printWidth": 120,
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"endOfLine": "lf"
}

View File

@ -1,16 +0,0 @@
# PDF-To-Markdown Converter Core
Javascript UI to parse PDF files and convert them into Markdown format. Online at http://pdf2md.morethan.io!
## Contribute
Use the [issue tracker](https://github.com/jzillmann/pdf-to-markdown/issues) and/or open [pull requests](https://github.com/jzillmann/pdf-to-markdown/pulls)!
## Build
- `npm install` Download all necessary npm packages
- `npm start` Run local development build
## Release
//TBD

9586
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,57 +0,0 @@
{
"name": "pdf-to-markdown",
"version": "0.2.0",
"description": "A PDF to Markdown Converter",
"keywords": [
"PDF",
"Markdown",
"Converter"
],
"author": "Johannes Zillmann",
"license": "AGPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/jzillmann/pdf-to-markdown"
},
"scripts": {
"start": "snowpack dev",
"build": "snowpack build",
"svelte-check": "svelte-check",
"test": "web-test-runner \"src/**/*.test.ts\""
},
"dependencies": {
"@fortawesome/free-solid-svg-icons": "5.15.2",
"pdfjs-dist": "2.6.347",
"simple-statistics": "^7.7.0",
"string-similarity": "4.0.4",
"svelte-file-dropzone": "0.0.15",
"uuid": "^8.3.2"
},
"devDependencies": {
"@snowpack/plugin-build-script": "^2.1.0",
"@snowpack/plugin-dotenv": "^2.0.5",
"@snowpack/plugin-svelte": "^3.5.2",
"@snowpack/plugin-typescript": "^1.2.1",
"@snowpack/web-test-runner-plugin": "^0.2.1",
"@testing-library/svelte": "^3.0.3",
"@types/chai": "^4.2.15",
"@types/snowpack-env": "^2.3.2",
"@web/test-runner": "^0.10.0",
"chai": "^4.3.0",
"fa-svelte": "^3.1.0",
"postcss": "^8.2.6",
"postcss-cli": "^8.3.1",
"postcss-import": "^14.0.0",
"postcss-load-config": "^3.0.1",
"prettier": "^2.2.1",
"prettier-plugin-svelte": "^1.4.2",
"rollup-plugin-svelte": "^7.1.0",
"snowpack": "^3.0.13",
"svelte": "^3.34.0",
"svelte-check": "^1.1.36",
"svelte-hero-icons": "^1.5.0",
"svelte-preprocess": "^4.6.9",
"tailwindcss": "^2.0.3",
"typescript": "^4.2.2"
}
}

View File

@ -1,7 +0,0 @@
module.exports = {
plugins: [
require("postcss-import"),
require("tailwindcss"),
require("autoprefixer"),
],
};

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="%PUBLIC_URL%" />
<meta charset="utf-8" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Converts PDF files to Markdown." />
<meta name="keywords" content="PDF, Markdown, converter, online" />
<title>PDF to Markdown</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="dist/index.js"></script>
<!--
This HTML file is a template.
-->
</body>
</html>

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,36 +0,0 @@
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
public: { url: '/', static: true },
src: { url: '/dist' },
'../core/src': { url: '/core' },
'node_modules/pdfjs-dist/es5/build/': { url: '/worker', static: true },
},
optimize: {
bundle: true,
minify: true,
target: 'es2020',
},
plugins: [
['@snowpack/plugin-build-script', { cmd: 'postcss', input: ['.css'], output: ['.css'] }],
'@snowpack/plugin-svelte',
'@snowpack/plugin-dotenv',
'@snowpack/plugin-typescript',
],
packageOptions: {
installTypes: true,
packageLookupFields: ['svelte', 'module', 'main'],
// rollup: { plugins: [require('rollup-plugin-svelte')()] },
},
devOptions: {
port: 3005,
},
buildOptions: {
baseUrl: '/pdf-to-markdown-staging/',
metaUrlPath: 'modules',
},
alias: {
'@core': '../core/src/index.js',
'@core/*': '../core/src/*',
},
};

View File

@ -1,46 +0,0 @@
<script>
import Upload from './main/Upload.svelte';
import { blur } from 'svelte/transition';
import { parseResult, debug } from './store';
import DebugView from './debug/DebugView.svelte';
</script>
<div in:blur={{ duration: 450 }} class="text-2xl font-semibold font-serif text-center bg-gray-400">
PDF to Markdown Converter
</div>
<main class="mt-2 h-full">
<div class="transition-container">
{#if $debug}
<span in:blur={{ duration: 1200 }} out:blur={{ duration: 900 }}>
<DebugView debug={$debug} />
</span>
{:else}
<span in:blur={{ duration: 900 }} out:blur={{ duration: 900 }}>
<Upload />
</span>
{/if}
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
@apply text-gray-800;
@apply bg-gray-50;
}
.transition-container {
display: grid;
grid-template-rows: 1;
grid-template-columns: 1;
}
.transition-container > * {
/* autoprefixer: off */
grid-row: 1;
/* autoprefixer: off */
grid-column: 1;
}
</style>

View File

@ -1,11 +0,0 @@
import {render} from '@testing-library/svelte';
import {expect} from 'chai';
import App from './App.svelte';
describe('<App>', () => {
it('renders learn svelte link', () => {
const {getByText} = render(App);
const linkElement = getByText(/learn svelte/i);
expect(document.body.contains(linkElement));
});
});

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,23 +0,0 @@
export function clickOutside(node: HTMLElement, { enabled: initialEnabled, cb }) {
const handleOutsideClick = ({ target }) => {
if (!node.contains(target)) {
cb();
}
};
function update({ enabled }) {
if (enabled) {
window.addEventListener('click', handleOutsideClick);
} else {
window.removeEventListener('click', handleOutsideClick);
}
}
update({ enabled: initialEnabled });
return {
update,
destroy() {
window.removeEventListener('click', handleOutsideClick);
},
};
}

View File

@ -1,21 +0,0 @@
export default function inView(node: HTMLElement) {
const handleIntersect = (e: IntersectionObserverEntry[]) => {
node.dispatchEvent(
new CustomEvent('intersect', {
detail: e[0].isIntersecting,
})
);
};
const root = null;
const rootMargin = `0px 0px 300px 0px`;
const options = { root, rootMargin };
const observer = new IntersectionObserver(handleIntersect, options);
observer.observe(node);
return {
destroy() {
if (observer) observer.disconnect();
},
};
}

View File

@ -1,28 +0,0 @@
<script>
export let name: string;
export let enabled = true;
</script>
<input id="checkbox-{name}" type="checkbox" class="hidden" bind:checked={enabled} />
<label
for="checkbox-{name}"
class="px-1 border-t-2 border-b-2 border-transparent cursor-pointer select-none whitespace-nowrap">{name}</label>
<style>
input:not(:checked) + label:hover {
transform: scale(1.1);
@apply text-select;
}
input:checked + label:hover {
transform: scale(1.1);
}
input:checked + label {
@apply border-select;
}
input + label:hover:not(:active) {
@apply border-dotted;
}
</style>

View File

@ -1,5 +0,0 @@
import type { SvelteComponent } from 'svelte';
export default class ComponentDefinition {
constructor(public component: object, public args: object = {}) {}
}

View File

@ -1,30 +0,0 @@
<script>
import { writable } from 'svelte/store';
import { setContext } from 'svelte';
import { clickOutside } from '../actions/clickOutside';
let opened = writable(false);
setContext('popupOpened', opened);
function toogle() {
opened.update((old) => !old);
}
</script>
<span use:clickOutside={{ enabled: opened, cb: () => opened.set(false) }}>
<span on:click={toogle}>
<slot name="trigger" opened={$opened} />
</span>
{#if $opened}
<span class="popupContent">
<slot name="content" class="z-20" />
</span>
{/if}
</span>
<style>
.popupContent :global(*) {
@apply z-30;
}
</style>

View File

@ -1,35 +0,0 @@
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
export let radius: number;
export let stroke: number;
export let progress: number;
const normalizedRadius = radius - stroke * 2;
const circumference = normalizedRadius * 2 * Math.PI;
const progressTweened = tweened(0, {
duration: 400,
easing: cubicOut,
});
$: progressTweened.set(progress);
$: strokeDashoffset = circumference - ($progressTweened / 100) * circumference;
</script>
<svg
height={radius * 2}
width={radius * 2}
class="text-select stroke-current"
style="filter: brightness({$progressTweened / 100 / 2 + 0.5}) sepia({0.5 - $progressTweened / 100 / 2}) blur({0.6 - $progressTweened / 100 / 3}px)">
<circle
fill="transparent"
stroke-width={stroke}
stroke-dasharray={circumference + ' ' + circumference}
stroke-dashoffset={strokeDashoffset}
r={normalizedRadius}
cx={radius}
cy={radius} />
<text x="50%" y="53%" text-anchor="middle" class="text-gray-800 fill-current" stroke-width="1px" dy=".2em">
{Math.round($progressTweened)}%
</text>
</svg>

View File

@ -1,22 +0,0 @@
import { Writable, writable } from 'svelte/store';
import { debugFromParams, debugStageFromParams } from './processParameters';
export const debugEnabled = storedWritable('debugEnabled', false, debugFromParams);
export const debugStage = storedWritable('debugStage', 0, debugStageFromParams);
function storedWritable<T>(key: string, defaultValue: T, paramLoadFunction: (defaultValue: T) => T): Writable<T> {
const value = paramLoadFunction(fromLocalStore(key, defaultValue));
const store = writable(value);
store.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
}
function fromLocalStore<T>(key: string, defaultValue: T): T {
const storedValue = localStorage.getItem(key);
if (storedValue === null || storedValue === 'null') {
return defaultValue;
}
return JSON.parse(storedValue);
}

View File

@ -1,65 +0,0 @@
<script>
import type EvaluationIndex from '@core/debug/EvaluationIndex';
import type ChangeIndex from '@core/debug/ChangeIndex';
import type Item from '@core/Item';
import { Addition, Removal, ContentChange, PositionChange, Direction } from '../../../core/src/debug/ChangeIndex';
import ComponentDefinition from '../components/ComponentDefinition';
import {
PlusCircle as Plus,
ExclamationCircle as Changed,
MinusCircle as Minus,
ArrowCircleUp as Up,
ArrowCircleDown as Down,
Eye,
} from 'svelte-hero-icons';
export let evaluations: EvaluationIndex;
export let changes: ChangeIndex;
export let item: Item;
$: evaluated = evaluations.evaluated(item);
$: hasChanged = changes.hasChanged(item);
let changeContent: string;
let iconComp: ComponentDefinition;
$: {
let args = { size: '14' };
if (hasChanged) {
let change = changes.change(item);
switch (change.constructor.name) {
case PositionChange.name:
const positionChange = change as PositionChange;
changeContent = `${positionChange.amount}`;
iconComp =
positionChange.direction === Direction.UP
? new ComponentDefinition(Up, args)
: new ComponentDefinition(Down, args);
break;
case Addition.name:
iconComp = new ComponentDefinition(Plus, args);
break;
case Removal.name:
iconComp = new ComponentDefinition(Minus, args);
break;
case ContentChange.name:
iconComp = new ComponentDefinition(Changed, args);
break;
default:
throw new Error(`${change.constructor.name}: ${change}`);
}
} else if (evaluated) {
iconComp = new ComponentDefinition(Eye, args);
}
}
</script>
{#if evaluated || hasChanged}
<div class="flex space-x-0.5 items-center text-xs">
{#if iconComp}
<svelte:component this={iconComp.component} {...iconComp.args} />
{/if}
{#if changeContent}
<div>{changeContent}</div>
{/if}
</div>
{/if}

View File

@ -1,99 +0,0 @@
<script>
import { slide } from 'svelte/transition';
import slideH from '../svelte/slideH';
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import { debugStage } from '../config';
import PageControl from './PageControl';
import Popup from '../components/Popup.svelte';
import PageSelectionPopup from './PageSelectionPopup.svelte';
import Checkbox from '../components/Checkbox.svelte';
import TransformerSelectionPopup from './TransformerSelectionPopup.svelte';
import FontEntry from './FontEntry.svelte';
export let stageNames: string[];
export let stageDescriptions: string[];
export let pageControl: PageControl;
export let fontMap: Map<string, object>;
export let supportsGrouping: boolean;
export let supportsRelevanceFiltering: boolean;
export let groupingEnabled = true;
export let onlyRelevantItems = true;
$: canNext = $debugStage + 1 < stageNames.length;
$: canPrev = $debugStage > 0;
</script>
<div class="sticky top-0 pt-2 pb-1 z-20 bg-gray-50">
<div class="flex items-center space-x-2">
<Popup>
<span slot="trigger" let:opened>
<BookOpen size="1x" class="hover:text-select cursor-pointer {opened && 'text-select'}" />
</span>
<span slot="content">
<PageSelectionPopup {pageControl} />
</span>
</Popup>
<Popup>
<span slot="trigger" let:opened>
<div
class="hover:text-select cursor-pointer {opened && 'text-select'}"
style="font-family: AmericanTypewriter, verdana">
F
</div>
</span>
<span slot="content">
<div class="absolute mt-1 py-2 px-2 bg-gray-200 rounded-br">
<div class=" overflow-y-scroll " style="max-height: 65vh" transition:slide={{ duration: 400 }}>
{#each [...fontMap.keys()] as fontName}
<FontEntry {fontMap} {fontName} />
{/each}
</div>
</div>
</span>
</Popup>
<div>|</div>
<div>Transformation:</div>
<span on:click={() => canPrev && debugStage.update((cur) => cur - 1)}>
<ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
<span on:click={() => canNext && debugStage.update((cur) => cur + 1)}>
<ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
<span>
<Popup>
<span slot="trigger">
<div class="w-52 cursor-pointer hover:underline whitespace-nowrap italic">
{stageNames[$debugStage]}
</div>
</span>
<span slot="content">
<TransformerSelectionPopup
{stageNames}
{stageDescriptions}
currentStage={$debugStage}
on:selectTransformer={({ detail }) => debugStage.set(detail)} />
</span>
</Popup>
</span>
<div class="w-full flex flex-row-reverse space-x-2 space-x-reverse text-sm">
{#if supportsGrouping}
<span class="inline-flex" transition:slideH={{ duration: 700 }}>
<Checkbox name="Grouped" bind:enabled={groupingEnabled} />
</span>
{/if}
{#if supportsRelevanceFiltering}
<span class="inline-flex" transition:slideH={{ duration: 700 }}>
<Checkbox name="Relevant Items" bind:enabled={onlyRelevantItems} />
</span>
{/if}
</div>
</div>
</div>
<!-- Little cushion sitting between the control bar and the item header. Relevant is the z-index. Item headers should go over it, item rows under! -->
<div class="sticky top-8 h-3 bg-gray-50" style="z-index:1" />

View File

@ -1,40 +0,0 @@
<script>
import slideH from '../svelte/slideH';
import { linear } from 'svelte/easing';
import Icon from 'fa-svelte';
import { faMapPin as pin } from '@fortawesome/free-solid-svg-icons/faMapPin';
import { ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import PageControl from './PageControl';
export let pageControl: PageControl;
export let pageIndex: number;
let { pagePinned, canPrev, canNext, pageMapping } = pageControl;
</script>
<div class="flex items-center space-x-1">
{#if $pagePinned}
<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} />
</span>
{/if}
<div>
Page
{$pageMapping.pageLabel(pageIndex)}
{#if $pageMapping.shifted()}
<span class="font-light"> ({pageIndex + 1}/{pageControl.totalPages}) </span>
{:else}/ {pageControl.totalPages + $pageMapping.pageFactor - 1}{/if}
</div>
</div>
{#if $pagePinned}
<div class="absolute flex ml-4 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}

View File

@ -1,93 +0,0 @@
<script>
import { slide, blur } from 'svelte/transition';
import { flip } from 'svelte/animate';
import type Debugger from '@core/Debugger';
import { PAGE_MAPPING } from '../../../core/src/transformer/CacluclateStatistics';
import { debugStage } from '../config';
import ControlBar from './ControlBar.svelte';
import ItemTable from './ItemTable.svelte';
import PageControl from './PageControl';
import CurrentPage from './CurrentPage.svelte';
export let debug: Debugger;
const pageControl = new PageControl(debug.pageCount);
const stageNames = debug.stageNames;
const { pinnedPageIndex, pagePinned } = pageControl;
let groupingEnabled = true;
let onlyRelevantItems = true;
$: stageResult = debug.stageResult($debugStage);
$: pageControl.updateMapping(stageResult.globals.getOptional(PAGE_MAPPING));
$: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger;
$: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll;
$: visiblePages = pageControl.selectPages(stageResult, onlyRelevantItems, groupingEnabled, $pinnedPageIndex);
</script>
<div class="mx-4">
<!-- <div>Parsed {parseResult.pageCount()} pages with {parseResult.items.length} items</div>
<div>Title: {parseResult.metadata.title()}</div>
<div>Author: {parseResult.metadata.author()}</div> -->
<!-- Sticky Controls -->
<ControlBar
{stageNames}
stageDescriptions={debug.stageDescriptions}
{pageControl}
fontMap={debug.fontMap}
{supportsGrouping}
{supportsRelevanceFiltering}
bind:groupingEnabled
bind:onlyRelevantItems />
<!-- Stage Messages -->
<ul class="messages list-disc list-inside mb-2 p-2 bg-blue-50 rounded shadow text-sm">
{#each stageResult.messages as message (message)}
<li animate:flip in:slide>{message}</li>
{/each}
</ul>
<!-- Items -->
{#if visiblePages.find((page) => page.itemGroups.length > 0)}
<ItemTable
schema={stageResult.schema}
pages={visiblePages}
{pageControl}
evaluations={stageResult.evaluations}
changes={stageResult.changes} />
{:else}
<!-- No items visible -->
<div class="flex mt-8">
{#if $pagePinned}
<span class="w-36">
<CurrentPage {pageControl} pageIndex={$pinnedPageIndex} />
</span>
{/if}
<div class="flex w-full space-x-1 items-center justify-center text-xl">
<div in:blur={{ delay: 500 }}>No visible changes from the transformation.</div>
{#if supportsRelevanceFiltering && onlyRelevantItems}
<div in:blur={{ delay: 900 }}>Disable the</div>
<div
in:blur={{ delay: 950 }}
class="font-bold cursor-pointer hover:underline"
on:click={() => (onlyRelevantItems = false)}>
relevance filter
</div>
<div in:blur={{ delay: 990 }}>?</div>
{/if}
{#if supportsGrouping && !groupingEnabled}
<div in:blur={{ delay: 1000 }}>Enable</div>
<div
in:blur={{ delay: 1050 }}
class="font-bold cursor-pointer hover:underline"
on:click={() => (groupingEnabled = true)}>
grouping
</div>
<div in:blur={{ delay: 1090 }}>?</div>
{/if}
</div>
</div>
{/if}
</div>

View File

@ -1,58 +0,0 @@
<script>
import { slide } from 'svelte/transition';
export let fontName: string;
export let fontMap: Map<string, object>;
let collapsed = true;
$: font = fontMap.get(fontName);
</script>
<div class="pb-1 rounded shadow bg-gray-300 min-w-max">
<div
class="twoColumned header py-1 px-2 text-sm bg-gray-400 rounded-t cursor-pointer"
class:opened={!collapsed}
on:click={() => (collapsed = !collapsed)}>
<div class="font-semibold">{fontName}</div>
<div class="">{font['name']}</div>
</div>
{#if !collapsed}
<div class="twoColumned px-2 text-sm" transition:slide>
<div>Type:</div>
<div>{font['type']}</div>
<div>MimeType:</div>
<div>{font['mimetype']}</div>
<div>Ascent:</div>
<div>{font['ascent'].toFixed(2)}</div>
<div>Descent:</div>
<div>{font['descent'].toFixed(2)}</div>
<div>BBox:</div>
<div>{font['bbox'].join(', ')}</div>
<div>Matrix:</div>
<div>{font['fontMatrix'].join(', ')}</div>
<div>Vertical:</div>
<div>{font['vertical']}</div>
<div>Monospace:</div>
<div>{font['isMonospace']}</div>
<div>Setif:</div>
<div>{font['isSerifFont']}</div>
<div>Type3:</div>
<div>{font['isType3Font']}</div>
</div>
{/if}
</div>
<style>
.twoColumned {
@apply grid;
@apply gap-x-2;
grid-template-columns: 1fr 2.5fr;
}
.header:hover {
@apply bg-select;
transform: scale(1.02);
}
.opened {
@apply bg-select;
}
</style>

View File

@ -1,135 +0,0 @@
<script>
import { fade } from 'svelte/transition';
import type ItemGroup from '@core/debug/ItemGroup';
import type EvaluationIndex from '@core/debug/EvaluationIndex';
import type ChangeIndex from '@core/debug/ChangeIndex';
import type AnnotatedColumn from '@core/debug/AnnotatedColumn';
import ChangeSymbol from './ChangeSymbol.svelte';
import { formatValue } from './formatValues';
import PageControl from './PageControl';
import CurrentPage from './CurrentPage.svelte';
export let pageControl: PageControl;
export let pageIdx: number;
export let itemIdx: number;
export let schema: AnnotatedColumn[];
export let itemGroup: ItemGroup;
export let evaluations: EvaluationIndex;
export let changes: ChangeIndex;
let expandedItemGroup: { pageIndex: number; itemIndex: number };
$: expanded =
expandedItemGroup && expandedItemGroup.pageIndex === pageIdx && expandedItemGroup.itemIndex === itemIdx;
const toggleRow = (pageIndex: number, itemIndex: number) => {
expandedItemGroup = expanded ? undefined : { pageIndex, itemIndex };
};
</script>
<tr
class:expandable={itemGroup.hasMany()}
class:expanded
class:changePlus={changes.isPlusChange(itemGroup.top)}
class:changeNeutral={changes.isNeutralChange(itemGroup.top)}
class:changeMinus={changes.isMinusChange(itemGroup.top)}
in:fade>
<!-- Page number in first page item row -->
{#if itemIdx === 0}
<td id="page" class="page bg-gray-50 align-top">
<CurrentPage pageIndex={pageIdx} {pageControl} />
</td>
{:else}
<td id="page" />
{/if}
<td class="align-baseline">
<ChangeSymbol {evaluations} {changes} item={itemGroup.top} />
</td>
{#if evaluations.hasScores()}
<td class="whitespace-nowrap">{evaluations.evaluationScore(itemGroup.top)}</td>
{/if}
<span class="contents" on:click={() => itemGroup.hasMany() && toggleRow(pageIdx, itemIdx)}>
<!-- ID & change marker column -->
<td class="align-top">
<div class="flex space-x-0.5 items-center">
<div>{itemIdx}{itemGroup.hasMany() ? '…' : ''}</div>
</div>
</td>
<!-- Row values -->
{#each schema as column}
<td class="select-all">{formatValue(itemGroup.top.data[column.name])}</td>
{/each}
</span>
</tr>
<!-- Expanded childs -->
{#if expanded}
{#each itemGroup.elements as child, childIdx}
<tr
class="childs"
class:changePlus={changes.isPlusChange(child)}
class:changeNeutral={changes.isNeutralChange(child)}
class:changeMinus={changes.isMinusChange(child)}>
<td id="page" />
<td class="align-baseline">
<ChangeSymbol {evaluations} {changes} item={child} />
</td>
{#if evaluations.hasScores()}
<td class="whitespace-nowrap">{evaluations.evaluationScore(itemGroup.top)}</td>
{/if}
<td class="whitespace-nowrap">
<div class="flex space-x-1">
<div class="w-8">{'└ ' + childIdx}</div>
</div>
</td>
{#each schema as column}
<td class="select-all">{formatValue(child.data[column.name])}</td>
{/each}
</tr>
{/each}
{/if}
<style>
.page {
@apply font-semibold;
@apply pr-4;
@apply whitespace-nowrap;
position: -webkit-sticky;
position: sticky;
top: 2.4em;
z-index: 2;
}
td:not(#page) {
@apply px-1;
@apply border-b;
}
tr:hover td:not(#page) {
@apply bg-gray-200;
}
tr.expandable:hover td:not(#page) {
@apply cursor-pointer;
}
tr.expanded td:not(#page) {
@apply bg-gray-300;
}
tr.childs td:not(#page) {
@apply bg-gray-200;
}
tr.changePlus td:not(#page) {
@apply text-green-700;
}
tr.changeMinus td:not(#page) {
@apply text-red-700;
}
tr.changeNeutral td:not(#page) {
@apply text-yellow-500;
}
</style>

View File

@ -1,113 +0,0 @@
<script>
import { scale } from 'svelte/transition';
import { PresentationChartLine } from 'svelte-hero-icons';
import type AnnotatedColumn from '@core/debug/AnnotatedColumn';
import type EvaluationIndex from '@core/debug/EvaluationIndex';
import type ChangeIndex from '@core/debug/ChangeIndex';
import type Page from '@core/debug/Page';
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
import inView from '../actions/inView';
import PageControl from './PageControl';
import ItemRow from './ItemRow.svelte';
export let schema: AnnotatedColumn[];
export let pages: Page[];
export let pageControl: PageControl;
export let evaluations: EvaluationIndex;
export let changes: ChangeIndex;
let { pagePinned } = pageControl;
let maxItemsToRenderInOneLoad = 200;
let renderedMaxPage = 0;
let renderedPages: Page[];
$: {
if ($pagePinned) {
renderedPages = pages;
renderedMaxPage = 0;
} else {
if (renderedMaxPage === 0) {
calculateNextPageToRenderTo();
}
renderedPages = pages.slice(0, renderedMaxPage);
}
}
function calculateNextPageToRenderTo() {
if (renderedMaxPage >= pages.length) {
return;
}
let itemCount = 0;
for (let index = 0; index < pages.length; index++) {
renderedMaxPage++;
itemCount += pages[index].itemGroups.length;
if (itemCount > maxItemsToRenderInOneLoad) {
break;
}
}
// console.log(`Render pages 0 to ${renderedMaxPage} with ${itemCount} items`);
}
</script>
<!-- Item table -->
<table class="w-full text-left">
<!-- Sticky header -->
<thead class="">
<th />
{#if changes.changeCount() > 0}
<th class="bg-gray-300 shadow">
<PresentationChartLine size="1x" />
</th>
{:else}
<th class="bg-gray-50" />
{/if}
{#if evaluations.hasScores()}
<th class="bg-gray-300 shadow">score</th>
{/if}
<th class="bg-gray-300 shadow">#</th>
{#each schema as column (column.name)}
<th
transition:scale
class="bg-gray-300 shadow {column.annotation === ColumnAnnotation.ADDED ? 'text-green-800' : column.annotation === ColumnAnnotation.REMOVED ? 'text-red-800' : ''} transition-colors duration-300 delay-200">
{column.name}
</th>
{/each}
</thead>
<tbody>
{#each renderedPages as page, pageIdx}
<!-- Separator between pages -->
{#if pageIdx > 0}
<tr class="h-5" />
{/if}
<!-- Page items -->
{#each page.itemGroups as itemGroup, itemIdx}
<ItemRow pageIdx={page.index} {itemIdx} {schema} {itemGroup} {evaluations} {changes} {pageControl} />
{/each}
{/each}
</tbody>
</table>
{#if $pagePinned}
<div class="mb-8" />
{:else}
{#if renderedMaxPage < pages.length}
<span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} />
<div class="my-6 text-center text-2xl">...</div>
{:else}
<div class="my-6 text-center text-2xl">FIN!</div>
{/if}
{/if}
<style>
th {
@apply px-1;
position: -webkit-sticky;
position: sticky;
top: 2.4em;
z-index: 2;
}
</style>

View File

@ -1,74 +0,0 @@
import StageResult from '@core/debug/StageResult';
import PageMapping from '../../../core/src/PageMapping';
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();
pageMapping: Writable<PageMapping> = writable(new PageMapping());
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);
}
updateMapping(pageMapping: PageMapping | undefined) {
if (pageMapping) {
this.pageMapping.set(pageMapping);
}
}
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);
}
}

View File

@ -1,55 +0,0 @@
<script>
import { slide } from 'svelte/transition';
import { getContext } from 'svelte';
import { Collection } from 'svelte-hero-icons';
import type { Writable } from 'svelte/store';
import PageControl from './PageControl';
export let pageControl: PageControl;
let { pinnedPageIndex, pagePinned, pageMapping } = pageControl;
let showIndex = false;
const popupOpened: Writable<boolean> = getContext('popupOpened');
function pinPage(index: number) {
popupOpened.set(false);
pageControl.pinPage(index);
}
function unpinPage() {
popupOpened.set(false);
pageControl.unpinPage();
}
</script>
<div
class="absolute my-2 p-2 flex bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96"
style="max-width: 77%;"
transition:slide>
<span class="mt-1 pr-2" on:click={$pagePinned && unpinPage}>
<Collection size="1x" class={$pagePinned ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
</span>
<div class="flex flex-wrap gap-1">
{#each new Array(pageControl.totalPages) as _, idx}
<div
class="flex-grow-0 px-2 w-12 max-w-xs 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'}"
on:click={() => pageControl.pageHasItems(idx) && pinPage(idx)}>
{#if showIndex}{idx}{:else}{$pageMapping.pageLabel(idx)}{/if}
</div>
{/each}
</div>
</div>
<style>
::-webkit-scrollbar {
-webkit-appearance: none;
width: 7px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
}
</style>

View File

@ -1,83 +0,0 @@
<script>
import { slide } from 'svelte/transition';
import { getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
import type { Writable } from 'svelte/store';
export let stageNames: string[];
export let stageDescriptions: string[];
export let currentStage: number;
const popupOpened: Writable<boolean> = getContext('popupOpened');
const dispatch = createEventDispatcher();
function selectTransformer(index: number) {
popupOpened.set(false);
dispatch('selectTransformer', index);
}
</script>
<div class="absolute -mt-6 " transition:slide>
<div class="p-2 bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96">
{#each stageNames as stageName, idx}
<div
on:click={() => selectTransformer(idx)}
class="tooltip px-2"
class:selected={idx == currentStage}
class:selectable={idx != currentStage}
data-text={stageDescriptions[idx]}>
{stageName}
</div>
{/each}
</div>
</div>
<style>
.selected {
@apply cursor-default;
@apply bg-gray-300;
@apply rounded;
}
.selectable {
@apply cursor-pointer;
}
.selectable:hover {
@apply bg-select;
@apply rounded;
}
.tooltip:before {
content: attr(data-text); /* here's the magic */
position: absolute;
transform: translateY(-2px);
/* move to right */
left: 100%;
/* the arrow */
border: 10px solid;
@apply border-select;
@apply border-t-transparent;
@apply border-r-transparent;
@apply border-b-transparent;
/* basic styles */
@apply ml-2;
@apply px-2;
@apply py-1;
@apply w-72;
@apply bg-gray-300;
@apply text-gray-800;
@apply rounded;
@apply shadow-sm;
@apply text-center;
@apply italic;
display: none; /* hide by default */
}
.tooltip:hover:before {
display: block;
}
</style>

View File

@ -1,19 +0,0 @@
export function formatValue(value: object) {
if (typeof value === 'undefined') {
return '';
}
if (Number.isInteger(value)) {
return value;
}
if (typeof value === 'number') {
return (value as number).toFixed(2);
}
if (typeof value === 'object' && typeof Array.isArray(value)) {
let array = value as Array<object>;
if (array.length > 0 && typeof array[0] === 'number') {
array = (array.map((element) => ((element as unknown) as number).toFixed(2)) as unknown) as Array<object>;
}
return '[' + array.join(', ') + ']';
}
return value;
}

View File

@ -1,18 +0,0 @@
import App from './App.svelte';
import './Tailwind.css';
var app = new App({
target: document.body,
intro: true,
});
export default app;
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://www.snowpack.dev/concepts/hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
app.$destroy();
});
}

View File

@ -1,158 +0,0 @@
<script>
import { blur, slide } from 'svelte/transition';
import Dropzone from 'svelte-file-dropzone';
import { Download, Check } from 'svelte-hero-icons';
import { processUpload, loadExample, loadUrl } from '../store';
import type Progress from '@core/Progress';
import ProgressRing from '../components/ProgressRing.svelte';
import Checkbox from '../components/Checkbox.svelte';
import { debugEnabled } from '../config';
import { urlFromParams } from '../processParameters';
let specifiedFileName: string;
let dragover = false;
let upload: Promise<any>;
let rejectionError: string;
let parseProgress: Progress;
const url = urlFromParams();
if (url) {
dragover = true;
specifiedFileName = url;
upload = loadUrl(handleProgress, url);
}
function handleUrlLoad() {
dragover = true;
let answer = prompt('Url of the pdf');
specifiedFileName = answer;
rejectionError = undefined;
parseProgress = undefined;
upload = loadUrl(handleProgress, answer);
}
function handleExampleLoad() {
dragover = true;
specifiedFileName = 'ExamplePdf.pdf';
rejectionError = undefined;
parseProgress = undefined;
upload = loadExample(handleProgress);
}
function handleFilesSelect(e) {
specifiedFileName = undefined;
rejectionError = undefined;
parseProgress = undefined;
const { acceptedFiles, fileRejections } = e.detail;
if (acceptedFiles.length === 1) {
const specifiedFile = acceptedFiles[0];
specifiedFileName = specifiedFile.name;
upload = processUpload(specifiedFile, handleProgress);
}
if (fileRejections.length > 1) {
const fileNames = fileRejections.map((r) => r.file.name);
rejectionError = `Only one file at a time allowed! Rejected ${fileRejections.length} files: '${fileNames}'.`;
}
}
function handleProgress(progress: Progress) {
parseProgress = progress;
}
</script>
<div class="container mx-auto">
<!-- Options -->
<div class="mb-0.5 flex flex-row-reverse space-x-2 space-x-reverse text-sm items-center">
<div class="py-0.5 border-2 border-gray-50 hover:underline cursor-pointer" on:click={handleExampleLoad}>
Load Example
</div>
<div class="py-0.5 border-2 border-gray-50 hover:underline cursor-pointer" on:click={handleUrlLoad}>
Open Url
</div>
<Checkbox name="Debug" bind:enabled={$debugEnabled} />
</div>
<!-- Upload Box -->
<div class="mb-5 border-2 border-dashed border-gray-400 hover:border-select" class:dragover>
<Dropzone
on:drop={handleFilesSelect}
on:dragenter={() => (dragover = true)}
on:dragleave={() => (dragover = false)}
multiple={false}
noClick={false}
disableDefaultStyles={true}>
<div class="grid grid-cols-1 md:grid-cols-2 justify-items-center">
<span class:dragoverItem={dragover}>
<Download size="21x" />
</span>
<div class="px-5 mb-5">
<div class="text-5xl font-bold my-4">Drop your PDF file here...</div>
<div class="text-2xl font-bold">Or click the box to select one...</div>
<div class="mt-14"><strong>Note:</strong> Your data stays locally in your browser.</div>
<div class="mt-5 text-sm italic font-serif">
This tool converts a PDF file into a Markdown text format! Simply drag & drop your PDF file on
the upload area and go from there. Don't expect wonders, there are a lot of variances in
generated PDF's from different tools and different ages. No matter how good the parser works for
your PDF, you will have to invest a good amount of manuell work to complete it.
</div>
</div>
</div>
</Dropzone>
</div>
<!-- Progress Info -->
<div class="mt-5 text-xl font-bold">
<div style="min-width: 70%;">
{#if specifiedFileName}
<div in:blur class="text-2xl mb-2">Parsing {specifiedFileName} ...</div>
{/if}
{#if parseProgress}
<div in:blur class="flex space-x-4">
<ProgressRing radius={50} stroke={7} progress={parseProgress?.totalProgress() * 100} />
<div>
{#each parseProgress.stages as stage, index}
{#if parseProgress.isProgressing(index)}
<div class="flex space-x-2 items-center">
<div>
Parsing
{stage}
{parseProgress.stageDetails[index] ? parseProgress.stageDetails[index] : ''}
</div>
</div>
{:else if parseProgress.isComplete(index)}
<div class="flex space-x-2 items-center ">
<div>
Parsing
{stage}
{parseProgress.stageDetails[index] ? parseProgress.stageDetails[index] : ''}
</div>
<Check size="1.5x" class="text-select" />
</div>
{/if}
{/each}
</div>
</div>
{/if}
{#if rejectionError}
<div in:slide class="text-red-700">{rejectionError}</div>
{/if}
{#await upload}
<!-- -->
{:catch error}
<div in:blur={{ delay: 200 }} class="text-red-700">
Failed to parse '{specifiedFileName}':
{error?.message}
</div>
{/await}
</div>
</div>
</div>
<style>
.dragover {
@apply border-select;
}
.dragoverItem {
@apply text-select;
}
</style>

View File

@ -1,29 +0,0 @@
const params = new URLSearchParams(window.location.search);
export function debugFromParams(defaultValue: boolean): boolean {
const defined = params.has('debug');
if (!defined) {
return defaultValue;
}
return params.get('debug') !== 'false';
}
export function debugStageFromParams(defaultValue: number): number {
if (!params.has('stage')) {
return defaultValue;
}
const stage = +params.get('stage');
if (!Number.isInteger(stage)) {
return defaultValue;
}
return stage;
}
export function urlFromParams(): string {
return getParameterByName('url');
}
function getParameterByName(name: string): string {
const match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}

View File

@ -1,48 +0,0 @@
import { createPipeline } from '@core';
import type ProgressListenFunction from '@core/ProgressListenFunction';
import type ParseResult from '@core/ParseResult';
import type Debugger from '@core/Debugger';
import * as pdfjs from 'pdfjs-dist/es5/build/pdf';
import { Writable, writable } from 'svelte/store';
export let debug: Writable<Debugger> = writable(undefined);
export let parseResult: Writable<ParseResult> = writable(undefined);
pdfjs.GlobalWorkerOptions.workerSrc = `worker/pdf.worker.min.js`;
const pdfPipeline = createPipeline(pdfjs, {});
export async function loadExample(progressListener: ProgressListenFunction): Promise<any> {
return parsePdf('ExamplePdf.pdf', progressListener);
}
export async function loadUrl(progressListener: ProgressListenFunction, url: String): Promise<any> {
return parsePdf('https://pdf-to-markdown-proxy.herokuapp.com/' + url, progressListener);
}
export async function processUpload(file: File, progressListener: ProgressListenFunction): Promise<any> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
resolve(reader.result as ArrayBuffer);
};
reader.readAsArrayBuffer(file);
}).then((buffer) => {
const data = new Uint8Array(buffer as ArrayBuffer);
return parsePdf(data, progressListener);
});
}
async function parsePdf(src: string | Uint8Array, progressListener: ProgressListenFunction): Promise<any> {
return pdfPipeline.debug(src, progressListener).then((debugInstance) => {
debug.set(debugInstance);
return debug;
});
//TODO without debug-flag
// return pdfPipeline.execute(src, progressListener).then((result) => {
// parseResult.set(result);
// return result;
// });
}

View File

@ -1,42 +0,0 @@
import { backInOut as easing } from 'svelte/easing';
const defaultOptions = {
delay: 0,
duration: 400,
easing,
};
/**
* Slide the element horizontally, across the X-axis.
* @param node
* @param options
*/
export default function slideH(node: HTMLElement, options: object) {
const mergedOptions = { ...defaultOptions, ...options };
const style = getComputedStyle(node);
const opacity = +style.opacity;
const width = parseFloat(style.width);
const padding_left = parseFloat(style.paddingLeft);
const padding_right = parseFloat(style.paddingRight);
const margin_left = parseFloat(style.marginLeft);
const margin_right = parseFloat(style.marginRight);
const border_left_width = parseFloat(style.borderLeftWidth);
const border_right_width = parseFloat(style.borderRightWidth);
return {
delay: mergedOptions.delay,
duration: mergedOptions.duration,
easing: mergedOptions.easing,
css: (t: number) =>
`overflow: hidden;` +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`width: ${t * width}px;` +
`padding-left: ${t * padding_left}px;` +
`padding-right: ${t * padding_right}px;` +
`margin-left: ${t * margin_left}px;` +
`margin-right: ${t * margin_right}px;` +
`border-left-width: ${t * border_left_width}px;` +
`border-right-width: ${t * border_right_width}px;`,
};
}

View File

@ -1,10 +0,0 @@
const autoPreprocess = require('svelte-preprocess');
module.exports = {
preprocess: autoPreprocess({
defaults: {
script: 'typescript',
},
postcss: true,
}),
};

View File

@ -1,47 +0,0 @@
const plugin = require('tailwindcss/plugin');
module.exports = {
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
purge: {
content: ['./src/**/*.svelte', './src/**/*.ts'],
},
theme: {
extend: {
colors: {
// gray-400 hue-rotated 180deg
select: '#A8A297',
},
gridTemplateColumns: {
15: 'repeat(15, minmax(0, 1fr))',
20: 'repeat(20, minmax(0, 1fr))',
},
},
},
variants: {
extend: {},
},
plugins: [
plugin(function ({ addUtilities, theme }) {
const themeColors = theme('colors');
const individualBorderColors = Object.keys(themeColors).map((colorName) => ({
[`.border-b-${colorName}`]: {
borderBottomColor: themeColors[colorName],
},
[`.border-t-${colorName}`]: {
borderTopColor: themeColors[colorName],
},
[`.border-l-${colorName}`]: {
borderLeftColor: themeColors[colorName],
},
[`.border-r-${colorName}`]: {
borderRightColor: themeColors[colorName],
},
}));
addUtilities(individualBorderColors);
}),
],
};

View File

@ -1,32 +0,0 @@
{
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
"jsx": "preserve",
"baseUrl": "./",
/* paths - If you configure Snowpack import aliases, add them here. */
"paths": {
"@core": ["../core/src"],
"@core/*": ["../core/src/*"]
},
/* noEmit - Snowpack builds (emits) files, not tsc. */
"noEmit": true,
/* Additional Options */
"strict": true,
"noImplicitAny": false,
// "noImplicitThis": false,
// "alwaysStrict": false,
// "strictBindCallApply": false,
"strictNullChecks": false,
// "strictFunctionTypes": false,
// "strictPropertyInitialization": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"useDefineForClassFields": true,
"allowSyntheticDefaultImports": true,
"importsNotUsedAsValues": "remove"
}
}

68
ui/types/static.d.ts vendored
View File

@ -1,68 +0,0 @@
/* Use this file to declare any custom file extensions for importing */
/* Use this folder to also add/extend a package d.ts file, if needed. */
declare module 'svelte-file-dropzone';
declare namespace svelte.JSX {
interface HTMLProps<T> {
// inView.ts action
onintersect?: (e: CustomEvent) => void;
}
}
/* CSS MODULES */
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.less' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.styl' {
const classes: { [key: string]: string };
export default classes;
}
/* CSS */
declare module '*.css';
declare module '*.scss';
declare module '*.sass';
declare module '*.less';
declare module '*.styl';
/* IMAGES */
declare module '*.svg' {
const ref: string;
export default ref;
}
declare module '*.bmp' {
const ref: string;
export default ref;
}
declare module '*.gif' {
const ref: string;
export default ref;
}
declare module '*.jpg' {
const ref: string;
export default ref;
}
declare module '*.jpeg' {
const ref: string;
export default ref;
}
declare module '*.png' {
const ref: string;
export default ref;
}
/* CUSTOM: ADD YOUR OWN HERE */

View File

@ -1,6 +0,0 @@
// NODE_ENV=test - Needed by "@snowpack/web-test-runner-plugin"
process.env.NODE_ENV = 'test';
module.exports = {
plugins: [require('@snowpack/web-test-runner-plugin')()],
};