Skip to content

Commit

Permalink
WIP: sheets
Browse files Browse the repository at this point in the history
  • Loading branch information
ananthakumaran committed Dec 25, 2023
1 parent 80d7e3f commit 9d04977
Show file tree
Hide file tree
Showing 11 changed files with 480 additions and 133 deletions.
220 changes: 185 additions & 35 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@cityssm/bulma-sticky-table": "^2.1.0",
"@codemirror/autocomplete": "^6.4.2",
"@codemirror/lang-javascript": "^6.2.1",
"@datasert/cronjs-matcher": "^1.2.0",
"@datasert/cronjs-parser": "^1.2.0",
"@egjs/svelte-grid": "^1.14.2",
Expand Down Expand Up @@ -46,6 +47,7 @@
"financial": "^0.1.3",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"mathjs": "^12.2.1",
"papaparse": "^5.4.1",
"pdfjs-dist": "^3.10.111",
"sprintf-js": "^1.1.2",
Expand Down
25 changes: 24 additions & 1 deletion src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ svg text {
fill: $grey-lighter;
}

.has-background-grey-lightest {
background-color: $grey-lightest;
}

.svg-text-grey-light {
fill: $grey-light;
}
Expand Down Expand Up @@ -388,6 +392,16 @@ svg text {
box-shadow: $shadow;
}

.box.box-r-none {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

.box.box-l-none {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

.box {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
}
Expand Down Expand Up @@ -536,7 +550,7 @@ nav.level.grid-2 {
}

.cm-activeLineGutter {
background-color: $grey-light !important;
background-color: $grey-lightest !important;
color: $grey-dark;
font-weight: bold;
}
Expand Down Expand Up @@ -693,6 +707,15 @@ nav.level.grid-2 {
height: 200px;
}

.sheet-result {
background-color: $white-ter;
color: $grey;
font-weight: bold;
}

.sheet-editor .cm-editor {
}

.search-query-editor {
border: 1px solid $grey-lighter;
border-radius: $radius;
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
}
const links: Link[] = [
{ label: "Dashboard", href: "/", hide: true },
{ label: "Sheets", href: "/sheets" },
{
label: "Cash Flow",
href: "/cash_flow",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/import.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";

import { parse, render, asRows } from "./sheet";
import { parse, render, asRows } from "./spreadsheet";
import fs from "fs";
import helpers from "./template_helpers";
import _ from "lodash";
Expand Down
154 changes: 60 additions & 94 deletions src/lib/sheet.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,69 @@
import Papa from "papaparse";
import * as XLSX from "xlsx";
import { parser } from "mathjs";
import { history, redoDepth, undoDepth } from "@codemirror/commands";
import { javascript } from "@codemirror/lang-javascript";
import { lintGutter } from "@codemirror/lint";
import type { Text } from "@codemirror/state";
import { EditorView } from "codemirror";
import _ from "lodash";
import { format } from "./journal";
import { pdf2array } from "./pdf";
import { sheetEditorState } from "../store";
import { basicSetup } from "./editor/base";
import { schedulePlugin } from "./transaction_tag";
import { formatCurrency, type SheetLineResult } from "./utils";

interface Result {
data: string[][];
}
export function createEditor(content: string, dom: Element) {
return new EditorView({
extensions: [
basicSetup,
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
javascript(),
lintGutter(),
history(),
EditorView.updateListener.of((viewUpdate) => {
const doc = viewUpdate.state.doc.toString();
const currentLine = viewUpdate.state.doc.lineAt(viewUpdate.state.selection.main.head);
sheetEditorState.update((current) => {
let results = current.results;
if (current.doc !== doc) {
results = evaluate(viewUpdate.state.doc);
}

export function parse(file: File): Promise<Result> {
let extension = file.name.split(".").pop();
extension = extension?.toLowerCase();
if (extension === "csv" || extension === "txt") {
return parseCSV(file);
} else if (extension === "xlsx" || extension === "xls") {
return parseXLSX(file);
} else if (extension === "pdf") {
return parsePDF(file);
}
throw new Error(`Unsupported file type ${extension}`);
}

export function asRows(result: Result): Array<Record<string, any>> {
return _.map(result.data, (row, i) => {
return _.chain(row)
.map((cell, j) => {
return [String.fromCharCode(65 + j), cell];
})
.concat([["index", i as any]])
.fromPairs()
.value();
return _.assign({}, current, {
results: results,
doc: doc,
currentLine: currentLine.number,
hasUnsavedChanges: current.hasUnsavedChanges || viewUpdate.docChanged,
undoDepth: undoDepth(viewUpdate.state),
redoDepth: redoDepth(viewUpdate.state)
});
});
}),
schedulePlugin
],
doc: content,
parent: dom
});
}

const COLUMN_REFS = _.chain(_.range(65, 90))
.map((i) => String.fromCharCode(i))
.map((a) => [a, a])
.fromPairs()
.value();

export function render(
rows: Array<Record<string, any>>,
template: Handlebars.TemplateDelegate,
options: { reverse?: boolean } = {}
) {
const output: string[] = [];
_.each(rows, (row) => {
const rendered = _.trim(template(_.assign({ ROW: row, SHEET: rows }, COLUMN_REFS)));
if (!_.isEmpty(rendered)) {
output.push(rendered);
function evaluate(doc: Text) {
const results: SheetLineResult[] = [];
const p = parser();
for (let i = 0; i < doc.lines; i++) {
const line = doc.line(i + 1);
console.log(line.text);
try {
let result = "";
const text = line.text.trim();
if (!_.isEmpty(text) && !text.startsWith("//")) {
result = p.evaluate(line.text);
}
if (_.isNumber(result)) {
result = formatCurrency(result);
}
results.push({ line: i + 1, error: false, result });
} catch (e) {
results.push({ line: i + 1, error: true, result: e.message });
}
});
if (options.reverse) {
output.reverse();
console.log(results[i]);
}
return format(output.join("\n\n"));
}

function parseCSV(file: File): Promise<Result> {
return new Promise((resolve, reject) => {
Papa.parse<string[]>(file, {
skipEmptyLines: true,
complete: function (results) {
resolve(results);
},
error: function (error) {
reject(error);
},
delimitersToGuess: [",", "\t", "|", ";", Papa.RECORD_SEP, Papa.UNIT_SEP, "^"]
});
});
}

async function parseXLSX(file: File): Promise<Result> {
const buffer = await readFile(file);
const sheet = XLSX.read(buffer, { type: "binary" });
const json = XLSX.utils.sheet_to_json<string[]>(sheet.Sheets[sheet.SheetNames[0]], {
header: 1,
blankrows: false,
rawNumbers: false
});
return { data: json };
}

async function parsePDF(file: File): Promise<Result> {
const buffer = await readFile(file);
const array = await pdf2array(buffer);
return { data: array };
}

function readFile(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result as ArrayBuffer);
};
reader.onerror = (event) => {
reject(event);
};
reader.readAsArrayBuffer(file);
});
return results;
}
103 changes: 103 additions & 0 deletions src/lib/spreadsheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Papa from "papaparse";
import * as XLSX from "xlsx";
import _ from "lodash";
import { format } from "./journal";
import { pdf2array } from "./pdf";

interface Result {
data: string[][];
}

export function parse(file: File): Promise<Result> {
let extension = file.name.split(".").pop();
extension = extension?.toLowerCase();
if (extension === "csv" || extension === "txt") {
return parseCSV(file);
} else if (extension === "xlsx" || extension === "xls") {
return parseXLSX(file);
} else if (extension === "pdf") {
return parsePDF(file);
}
throw new Error(`Unsupported file type ${extension}`);
}

export function asRows(result: Result): Array<Record<string, any>> {
return _.map(result.data, (row, i) => {
return _.chain(row)
.map((cell, j) => {
return [String.fromCharCode(65 + j), cell];
})
.concat([["index", i as any]])
.fromPairs()
.value();
});
}

const COLUMN_REFS = _.chain(_.range(65, 90))
.map((i) => String.fromCharCode(i))
.map((a) => [a, a])
.fromPairs()
.value();

export function render(
rows: Array<Record<string, any>>,
template: Handlebars.TemplateDelegate,
options: { reverse?: boolean } = {}
) {
const output: string[] = [];
_.each(rows, (row) => {
const rendered = _.trim(template(_.assign({ ROW: row, SHEET: rows }, COLUMN_REFS)));
if (!_.isEmpty(rendered)) {
output.push(rendered);
}
});
if (options.reverse) {
output.reverse();
}
return format(output.join("\n\n"));
}

function parseCSV(file: File): Promise<Result> {
return new Promise((resolve, reject) => {
Papa.parse<string[]>(file, {
skipEmptyLines: true,
complete: function (results) {
resolve(results);
},
error: function (error) {
reject(error);
},
delimitersToGuess: [",", "\t", "|", ";", Papa.RECORD_SEP, Papa.UNIT_SEP, "^"]
});
});
}

async function parseXLSX(file: File): Promise<Result> {
const buffer = await readFile(file);
const sheet = XLSX.read(buffer, { type: "binary" });
const json = XLSX.utils.sheet_to_json<string[]>(sheet.Sheets[sheet.SheetNames[0]], {
header: 1,
blankrows: false,
rawNumbers: false
});
return { data: json };
}

async function parsePDF(file: File): Promise<Result> {
const buffer = await readFile(file);
const array = await pdf2array(buffer);
return { data: array };
}

function readFile(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result as ArrayBuffer);
};
reader.onerror = (event) => {
reject(event);
};
reader.readAsArrayBuffer(file);
});
}
6 changes: 6 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ export interface GoalSummary {
priority: number;
}

export interface SheetLineResult {
line: number;
result: string;
error: boolean;
}

const tokenKey = "token";

const BACKGROUND = [
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/ledger/import/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
updateContent as updatePreviewContent
} from "$lib/editor";
import Dropzone from "svelte-file-dropzone/Dropzone.svelte";
import { parse, asRows, render as renderJournal } from "$lib/sheet";
import { parse, asRows, render as renderJournal } from "$lib/spreadsheet";
import _ from "lodash";
import type { EditorView } from "codemirror";
import { onMount } from "svelte";
Expand Down
Loading

0 comments on commit 9d04977

Please sign in to comment.