feat: support for multiple encodings in CSV files (#5756)

This commit is contained in:
Ariel Leyva
2026-02-14 01:37:28 -05:00
committed by GitHub
parent 88b97def9e
commit f67bccf8c5
11 changed files with 276 additions and 321 deletions

View File

@@ -21,6 +21,7 @@
"@vueuse/core": "^14.0.0",
"@vueuse/integrations": "^14.0.0",
"ace-builds": "^1.43.2",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
"dompurify": "^3.2.6",
"epubjs": "^0.3.93",

184
frontend/pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
ace-builds:
specifier: ^1.43.2
version: 1.43.6
csv-parse:
specifier: ^6.1.0
version: 6.1.0
dayjs:
specifier: ^1.11.13
version: 1.11.19
@@ -705,24 +708,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.27.2':
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.27.2':
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
@@ -753,12 +744,6 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.27.2':
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
@@ -801,24 +786,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.27.2':
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.27.2':
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
@@ -837,24 +810,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.27.2':
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.27.2':
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
@@ -945,60 +906,30 @@ packages:
cpu: [arm64]
os: [openharmony]
'@esbuild/openharmony-arm64@0.27.2':
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.27.2':
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.27.2':
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.27.2':
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.27.2':
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1170,26 +1101,11 @@ packages:
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.55.1':
resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.55.1':
resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.55.1':
resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.55.1':
resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.55.1':
resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==}
cpu: [x64]
@@ -1207,18 +1123,6 @@ packages:
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.55.1':
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.55.1':
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.55.1':
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
cpu: [loong64]
@@ -1283,11 +1187,6 @@ packages:
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.55.1':
resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.55.1':
resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==}
cpu: [ia32]
@@ -1854,6 +1753,9 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
csv-parse@6.1.0:
resolution: {integrity: sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==}
custom-error-instance@2.1.1:
resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==}
@@ -3605,15 +3507,9 @@ snapshots:
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm64@0.27.2':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-arm@0.27.2':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
@@ -3629,9 +3525,6 @@ snapshots:
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.27.2':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
@@ -3653,15 +3546,9 @@ snapshots:
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-arm@0.27.2':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-ia32@0.27.2':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
@@ -3671,15 +3558,9 @@ snapshots:
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.27.2':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.27.2':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
@@ -3725,33 +3606,18 @@ snapshots:
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.27.2':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.27.2':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.27.2':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-ia32@0.27.2':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@esbuild/win32-x64@0.27.2':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)':
dependencies:
eslint: 9.39.2
@@ -3926,18 +3792,9 @@ snapshots:
'@rollup/rollup-android-arm-eabi@4.55.1':
optional: true
'@rollup/rollup-android-arm64@4.55.1':
optional: true
'@rollup/rollup-darwin-arm64@4.55.1':
optional: true
'@rollup/rollup-darwin-x64@4.55.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.55.1':
optional: true
'@rollup/rollup-freebsd-x64@4.55.1':
optional: true
@@ -3947,12 +3804,6 @@ snapshots:
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.55.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.55.1':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.55.1':
optional: true
@@ -3986,9 +3837,6 @@ snapshots:
'@rollup/rollup-openharmony-arm64@4.55.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.55.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.55.1':
optional: true
@@ -4670,6 +4518,8 @@ snapshots:
csstype@3.2.3: {}
csv-parse@6.1.0: {}
custom-error-instance@2.1.1: {}
d@1.0.2:
@@ -4759,19 +4609,12 @@ snapshots:
esbuild@0.27.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2
'@esbuild/android-arm': 0.27.2
'@esbuild/android-arm64': 0.27.2
'@esbuild/android-x64': 0.27.2
'@esbuild/darwin-arm64': 0.27.2
'@esbuild/darwin-x64': 0.27.2
'@esbuild/freebsd-arm64': 0.27.2
'@esbuild/freebsd-x64': 0.27.2
'@esbuild/linux-arm': 0.27.2
'@esbuild/linux-arm64': 0.27.2
'@esbuild/linux-ia32': 0.27.2
'@esbuild/linux-loong64': 0.27.2
'@esbuild/linux-mips64el': 0.27.2
'@esbuild/linux-ppc64': 0.27.2
'@esbuild/linux-riscv64': 0.27.2
'@esbuild/linux-s390x': 0.27.2
'@esbuild/linux-x64': 0.27.2
@@ -4779,11 +4622,6 @@ snapshots:
'@esbuild/netbsd-x64': 0.27.2
'@esbuild/openbsd-arm64': 0.27.2
'@esbuild/openbsd-x64': 0.27.2
'@esbuild/openharmony-arm64': 0.27.2
'@esbuild/sunos-x64': 0.27.2
'@esbuild/win32-arm64': 0.27.2
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
escalade@3.2.0: {}
@@ -5392,15 +5230,10 @@ snapshots:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.55.1
'@rollup/rollup-android-arm64': 4.55.1
'@rollup/rollup-darwin-arm64': 4.55.1
'@rollup/rollup-darwin-x64': 4.55.1
'@rollup/rollup-freebsd-arm64': 4.55.1
'@rollup/rollup-freebsd-x64': 4.55.1
'@rollup/rollup-linux-arm-gnueabihf': 4.55.1
'@rollup/rollup-linux-arm-musleabihf': 4.55.1
'@rollup/rollup-linux-arm64-gnu': 4.55.1
'@rollup/rollup-linux-arm64-musl': 4.55.1
'@rollup/rollup-linux-loong64-gnu': 4.55.1
'@rollup/rollup-linux-loong64-musl': 4.55.1
'@rollup/rollup-linux-ppc64-gnu': 4.55.1
@@ -5412,7 +5245,6 @@ snapshots:
'@rollup/rollup-linux-x64-musl': 4.55.1
'@rollup/rollup-openbsd-x64': 4.55.1
'@rollup/rollup-openharmony-arm64': 4.55.1
'@rollup/rollup-win32-arm64-msvc': 4.55.1
'@rollup/rollup-win32-ia32-msvc': 4.55.1
'@rollup/rollup-win32-x64-gnu': 4.55.1
'@rollup/rollup-win32-x64-msvc': 4.55.1

View File

@@ -3,14 +3,25 @@ import { useLayoutStore } from "@/stores/layout";
import { baseURL } from "@/utils/constants";
import { upload as postTus, useTus } from "./tus";
import { createURL, fetchURL, removePrefix, StatusError } from "./utils";
import { isEncodableResponse, makeRawResource } from "@/utils/encodings";
export async function fetch(url: string, signal?: AbortSignal) {
const encoding = isEncodableResponse(url);
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, { signal });
const res = await fetchURL(`/api/resources${url}`, {
signal,
headers: {
"X-Encoding": encoding ? "true" : "false",
},
});
let data: Resource;
try {
data = (await res.json()) as Resource;
if (res.headers.get("Content-Type") == "application/octet-stream") {
data = await makeRawResource(res, url);
} else {
data = (await res.json()) as Resource;
}
} catch (e) {
// Check if the error is an intentional cancellation
if (e instanceof Error && e.name === "AbortError") {

View File

@@ -4,35 +4,13 @@
<i class="material-icons">error</i>
<p>{{ displayError }}</p>
</div>
<div v-else-if="data.headers.length === 0" class="csv-empty">
<div v-else-if="parsed.headers.length === 0" class="csv-empty">
<i class="material-icons">description</i>
<p>{{ $t("files.lonely") }}</p>
</div>
<div v-else class="csv-table-container" @wheel.stop @touchmove.stop>
<table class="csv-table">
<thead>
<tr>
<th v-for="(header, index) in data.headers" :key="index">
{{ header || `Column ${index + 1}` }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data.rows" :key="rowIndex">
<td v-for="(cell, cellIndex) in row" :key="cellIndex">
{{ cell }}
</td>
</tr>
</tbody>
</table>
<div class="csv-footer">
<div class="csv-info" v-if="data.rows.length > 100">
<i class="material-icons">info</i>
<span>
{{ $t("files.showingRows", { count: data.rows.length }) }}</span
>
</div>
<div class="column-separator">
<div class="csv-header">
<div class="header-select">
<label for="columnSeparator">{{ $t("files.columnSeparator") }}</label>
<select
id="columnSeparator"
@@ -50,17 +28,61 @@
</option>
</select>
</div>
<div class="header-select" v-if="isEncodedContent">
<label for="fileEncoding">{{ $t("files.fileEncoding") }}</label>
<select
id="fileEncoding"
class="input input--block"
v-model="selectedEncoding"
>
<option
v-for="encoding in availableEncodings"
:value="encoding"
:key="encoding"
>
{{ encoding }}
</option>
</select>
</div>
</div>
<table class="csv-table">
<thead>
<tr>
<th v-for="(header, index) in parsed.headers" :key="index">
{{ header || `Column ${index + 1}` }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in parsed.rows" :key="rowIndex">
<td v-for="(cell, cellIndex) in row" :key="cellIndex">
{{ cell }}
</td>
</tr>
</tbody>
</table>
<div class="csv-footer">
<div class="csv-info" v-if="parsed.rows.length > 100">
<i class="material-icons">info</i>
<span>
{{ $t("files.showingRows", { count: parsed.rows.length }) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { parseCSV, type CsvData } from "@/utils/csv";
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
import { parse } from "csv-parse/browser/esm";
import { useI18n } from "vue-i18n";
import { availableEncodings, decode } from "@/utils/encodings";
const { t } = useI18n({});
interface Props {
content: string;
content: ArrayBuffer | string;
error?: string;
}
@@ -68,31 +90,43 @@ const props = withDefaults(defineProps<Props>(), {
error: "",
});
const columnSeparator = ref([","]);
const columnSeparator = ref([",", ";"]);
const data = computed<CsvData>(() => {
try {
return parseCSV(props.content, columnSeparator.value);
} catch (e) {
console.error("Failed to parse CSV:", e);
return { headers: [], rows: [] };
}
const selectedEncoding = ref("utf-8");
const parsed = ref<CsvData>({ headers: [], rows: [] });
const displayError = ref<string | null>(null);
const isEncodedContent = computed(() => {
return props.content instanceof ArrayBuffer;
});
const displayError = computed(() => {
// External error takes priority (e.g., file too large)
if (props.error) {
return props.error;
watchEffect(() => {
if (props.content !== "" && columnSeparator.value.length > 0) {
const content = isEncodedContent.value
? decode(props.content as ArrayBuffer, selectedEncoding.value)
: props.content;
parse(
content as string,
{ delimiter: columnSeparator.value, skip_empty_lines: true },
(error, output) => {
if (error) {
console.error("Failed to parse CSV:", error);
parsed.value = { headers: [], rows: [] };
displayError.value = t("files.csvLoadFailed", {
error: error.toString(),
});
} else {
parsed.value = {
headers: output[0],
rows: output.slice(1),
};
displayError.value = null;
}
}
);
}
// Check for parse errors
if (
props.content &&
props.content.trim().length > 0 &&
data.value.headers.length === 0
) {
return "Failed to parse CSV file";
}
return null;
});
</script>
@@ -213,10 +247,6 @@ const displayError = computed(() => {
padding: 0.5rem;
}
.csv-footer > :only-child {
margin-left: auto;
}
.csv-info {
display: flex;
align-items: center;
@@ -230,18 +260,25 @@ const displayError = computed(() => {
font-size: 0.875rem;
}
.column-separator {
.csv-header {
display: flex;
justify-content: space-between;
padding: 0.25rem;
}
.header-select {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.column-separator > label {
.header-select > label {
font-size: small;
text-align: end;
max-width: 80px;
}
.column-separator > select {
.header-select > select {
margin-bottom: 0;
}

View File

@@ -88,7 +88,8 @@
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "select file or directory",

View File

@@ -21,6 +21,7 @@ interface Resource extends ResourceBase {
index: number;
subtitles?: string[];
content?: string;
rawContent?: ArrayBuffer;
}
interface ResourceItem extends ResourceBase {
@@ -57,3 +58,8 @@ interface BreadCrumb {
name: string;
url: string;
}
interface CsvData {
headers: string[];
rows: string[][];
}

View File

@@ -1,64 +0,0 @@
export interface CsvData {
headers: string[];
rows: string[][];
}
/**
* Parse CSV content into headers and rows
* Supports quoted fields and handles commas within quotes
*/
export function parseCSV(
content: string,
columnSeparator: Array<string>
): CsvData {
if (!content || content.trim().length === 0) {
return { headers: [], rows: [] };
}
const lines = content.split(/\r?\n/);
const result: string[][] = [];
for (const line of lines) {
if (line.trim().length === 0) continue;
const row: string[] = [];
let currentField = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
// Escaped quote
currentField += '"';
i++; // Skip next quote
} else {
// Toggle quote state
inQuotes = !inQuotes;
}
} else if (columnSeparator.includes(char) && !inQuotes) {
// Field separator
row.push(currentField);
currentField = "";
} else {
currentField += char;
}
}
// Add the last field
row.push(currentField);
result.push(row);
}
if (result.length === 0) {
return { headers: [], rows: [] };
}
// First row is headers
const headers = result[0];
const rows = result.slice(1);
return { headers, rows };
}

View File

@@ -0,0 +1,95 @@
export const availableEncodings = [
"utf-8",
"ibm866",
"iso-8859-2",
"iso-8859-3",
"iso-8859-4",
"iso-8859-5",
"iso-8859-6",
"iso-8859-7",
"iso-8859-8",
"iso-8859-8-i",
"iso-8859-10",
"iso-8859-13",
"iso-8859-14",
"iso-8859-15",
"iso-8859-16",
"koi8-r",
"koi8-u",
"macintosh",
"windows-874",
"windows-1250",
"windows-1251",
"windows-1252",
"windows-1253",
"windows-1254",
"windows-1255",
"windows-1256",
"windows-1257",
"windows-1258",
"x-mac-cyrillic",
"gbk",
"gb18030",
"big5",
"euc-jp",
"iso-2022-jp",
"shift_jis",
"euc-kr",
"utf-16be",
"utf-16le",
];
export function decode(content: ArrayBuffer, encoding: string): string {
const decoder = new TextDecoder(encoding);
return decoder.decode(content);
}
export function isEncodableResponse(url: string): boolean {
const extensions = [".csv"];
if (typeof TextDecoder === "undefined") {
return false;
}
for (const extension of extensions) {
if (url.endsWith(extension)) {
return true;
}
}
return false;
}
export async function makeRawResource(
res: Response,
url: string
): Promise<Resource> {
const buffer = await res.arrayBuffer();
return {
items: [],
numDirs: 0,
numFiles: 0,
sorting: {} as Sorting,
index: 0,
extension: getExtension(url),
isDir: false,
isSymlink: false,
path: url,
size: buffer.byteLength,
modified: new Date().toISOString(),
name: url.split("/").pop() || "",
type: "text",
mode: 0,
url: `/files${url}`,
rawContent: buffer,
content: decode(buffer, "utf-8"),
};
}
function getExtension(url: string): string {
const lastDotIndex = url.lastIndexOf(".");
if (lastDotIndex === -1) {
return "";
}
return url.substring(lastDotIndex);
}

View File

@@ -171,25 +171,19 @@ onMounted(() => {
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
);
editor.value = ace.edit("editor", {
value: fileContent,
showPrintMargin: false,
readOnly: fileStore.req?.type === "textImmutable",
theme: getEditorTheme(authStore.user?.aceEditorTheme ?? ""),
mode: modelist.getModeForPath(fileStore.req!.name).mode,
wrap: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
});
editor.value.setFontSize(fontSize.value);
editor.value.focus();
editor.value.getSelection().on("changeSelection", () => {
isSelectionEmpty.value =
editor.value == null || editor.value.getSelectedText().length == 0;
});
if (!layoutStore.loading) {
initEditor(fileContent);
} else {
const unwatch = watchEffect(() => {
// Initialize editor when layout is loaded
if (!layoutStore.loading) {
setTimeout(() => {
initEditor(fileContent);
unwatch();
}, 50);
}
});
}
});
onBeforeUnmount(() => {
@@ -218,6 +212,23 @@ onBeforeRouteUpdate((to, from, next) => {
});
});
const initEditor = (fileContent: string) => {
editor.value = ace.edit("editor", {
value: fileContent,
showPrintMargin: false,
readOnly: fileStore.req?.type === "textImmutable",
theme: getEditorTheme(authStore.user?.aceEditorTheme ?? ""),
mode: modelist.getModeForPath(fileStore.req!.name).mode,
wrap: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
});
editor.value.setFontSize(fontSize.value);
editor.value.focus();
};
const keyEvent = (event: KeyboardEvent) => {
if (event.code === "Escape") {
close();

View File

@@ -253,7 +253,7 @@ const hoverNav = ref<boolean>(false);
const autoPlay = ref<boolean>(false);
const previousRaw = ref<string>("");
const nextRaw = ref<string>("");
const csvContent = ref<string>("");
const csvContent = ref<ArrayBuffer | string>("");
const csvError = ref<string>("");
const player = ref<HTMLVideoElement | HTMLAudioElement | null>(null);
@@ -393,7 +393,11 @@ const updatePreview = async () => {
if (fileStore.req.size > CSV_MAX_SIZE) {
csvError.value = t("files.csvTooLarge");
} else {
csvContent.value = fileStore.req.content ?? "";
if (fileStore.req.rawContent != null) {
csvContent.value = fileStore.req.rawContent;
} else {
csvContent.value = fileStore.req.content ?? "";
}
}
}