mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
Extending QuickPreview
functionality with additional filetype support (#1231)
* added some files `standard` mime type * Used `TEXTViewer` Component to show Code Preview * Update Thumb.tsx * added `prismjs` * removed unnecessary comment * `CODEViewer` Component for Syntax Highlighting * formatting * using **Atom** Theme for `Prism` * merge text/code viewers & bg-app-focus for prism currently calling onError and onLoad without an Event argument that should change but i'm not really sure what to do there * removed unused imports * Update index.ts * `TEXTViewer` to `TextViewer_` * `TextViewer_` to `TextViewer` * Don't highlight normal TextFiles * clean code * `TEXTViewer` to `TextViewer` * using tailwind classes more * doing things correctly. * installed `prismjs` in interface * using own scroller * Update Thumb.tsx * Add an AbortController to the fetch request - Fix onError and onLoad calls - Format code * Fix onError being called when request was aborted due to re-render - Fix Compoenent re-rendering loop due to circular reference in useEffect - Remove unused imports * Improve text file serving and code syntax highlight - Implement way to identify text files in file-ext crate - Do not depend only on the file extension to identify text files in custom_uri - Import more prismjs language rules files - Add line numbers to TextViewer when rendering code * Clippy and prettier * Fix reading zero byte data to Vec - Improve empty file handling * Expand code highlight to more file types - Fix 10MB when it should be 10KB - Add supported for more code and config files extensions to sd-file-ext - Add comlink and vite-plugin-comlink for easy js worker integration - Move Prismjs logic to a Worker, because larger files (1000+ lines) where causing the UI to hang - Replace line-number prismjs plugin with our own implementation * Fix uppercase extension name --------- Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com> Co-authored-by: pr <pineapplerind.info@gmail.com> Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com>
This commit is contained in:
parent
1184d29379
commit
08ba4f917a
|
@ -19,5 +19,8 @@ apps/desktop/src/index.tsx
|
|||
/packages/client/src/core.ts
|
||||
apps/desktop/src/commands.ts
|
||||
|
||||
# Import only file, which order is relevant
|
||||
interface/components/TextViewer/prism.ts
|
||||
|
||||
.next/
|
||||
.contentlayer/
|
|
@ -21,7 +21,6 @@ module.exports = {
|
|||
],
|
||||
importOrderSortSpecifiers: true,
|
||||
importOrderParserPlugins: ['importAssertions', 'typescript', 'jsx'],
|
||||
pluginSearchDirs: false,
|
||||
plugins: ['@trivago/prettier-plugin-sort-imports', 'prettier-plugin-tailwindcss'],
|
||||
tailwindConfig: './packages/ui/tailwind.config.js'
|
||||
};
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
"@sd/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^4.24.4",
|
||||
"@tauri-apps/api": "1.3.0",
|
||||
"comlink": "^4.4.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "6.9.0",
|
||||
"vite-plugin-html": "^3.2.0"
|
||||
"react-router-dom": "6.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
|
@ -40,6 +40,8 @@
|
|||
"typescript": "^5.0.4",
|
||||
"vite": "^4.0.4",
|
||||
"vite-plugin-svgr": "^2.2.1",
|
||||
"vite-tsconfig-paths": "^4.0.3"
|
||||
"vite-tsconfig-paths": "^4.0.3",
|
||||
"vite-plugin-comlink": "^3.0.5",
|
||||
"vite-plugin-html": "^3.2.0"
|
||||
}
|
||||
}
|
||||
|
|
1
apps/desktop/src/vite-env.d.ts
vendored
1
apps/desktop/src/vite-env.d.ts
vendored
|
@ -1,4 +1,5 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-comlink/client" />
|
||||
|
||||
declare interface ImportMetaEnv {
|
||||
VITE_OS: string;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Plugin, mergeConfig } from 'vite';
|
||||
import { comlink } from 'vite-plugin-comlink';
|
||||
import baseConfig from '../../packages/config/vite';
|
||||
|
||||
const devtoolsPlugin: Plugin = {
|
||||
|
@ -20,5 +21,8 @@ export default mergeConfig(baseConfig, {
|
|||
server: {
|
||||
port: 8001
|
||||
},
|
||||
plugins: [devtoolsPlugin]
|
||||
plugins: [devtoolsPlugin, comlink()],
|
||||
worker: {
|
||||
plugins: [comlink()]
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
};
|
||||
|
||||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
mem::take,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -13,9 +14,6 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::cmp::min;
|
||||
|
||||
use http_range::HttpRange;
|
||||
use httpz::{
|
||||
http::{response::Builder, Method, Response, StatusCode},
|
||||
|
@ -24,6 +22,7 @@ use httpz::{
|
|||
use mini_moka::sync::Cache;
|
||||
use once_cell::sync::Lazy;
|
||||
use prisma_client_rust::QueryError;
|
||||
use sd_file_ext::text::is_text;
|
||||
use thiserror::Error;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
|
@ -39,6 +38,8 @@ type NameAndExtension = (PathBuf, String);
|
|||
static FILE_METADATA_CACHE: Lazy<Cache<MetadataCacheKey, NameAndExtension>> =
|
||||
Lazy::new(|| Cache::new(100));
|
||||
|
||||
static MAX_TEXT_READ_LENGHT: usize = 10 * 1024; // 10KB
|
||||
|
||||
// TODO: We should listen to events when deleting or moving a location and evict the cache accordingly.
|
||||
// TODO: Probs use this cache in rspc queries too!
|
||||
|
||||
|
@ -206,7 +207,7 @@ async fn handle_file(
|
|||
lru_entry
|
||||
};
|
||||
|
||||
let file = File::open(&file_path_full_path).await.map_err(|err| {
|
||||
let mut file = File::open(&file_path_full_path).await.map_err(|err| {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
HandleCustomUriError::NotFound("file")
|
||||
} else {
|
||||
|
@ -214,9 +215,11 @@ async fn handle_file(
|
|||
}
|
||||
})?;
|
||||
|
||||
let extension = extension.as_str();
|
||||
|
||||
// TODO: This should be determined from magic bytes when the file is indexed and stored it in the DB on the file path
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
let mime_type = match extension.as_str() {
|
||||
let mime_type = match extension {
|
||||
// AAC audio
|
||||
"aac" => "audio/aac",
|
||||
// Musical Instrument Digital Interface (MIDI)
|
||||
|
@ -278,13 +281,7 @@ async fn handle_file(
|
|||
"heic" | "heics" => "image/heic,image/heic-sequence",
|
||||
// AVIF images
|
||||
"avif" | "avci" | "avcs" => "image/avif",
|
||||
// TEXT document
|
||||
"txt" => "text/plain",
|
||||
_ => {
|
||||
return Err(HandleCustomUriError::BadRequest(
|
||||
"TODO: This filetype is not supported because of the missing mime type!",
|
||||
));
|
||||
}
|
||||
_ => "text/plain",
|
||||
};
|
||||
|
||||
let mut content_lenght = file
|
||||
|
@ -293,6 +290,55 @@ async fn handle_file(
|
|||
.map_err(|e| FileIOError::from((&file_path_full_path, e)))?
|
||||
.len();
|
||||
|
||||
let mime_type = if mime_type == "text/plain" {
|
||||
let mut text_buf = vec![0; min(content_lenght as usize, MAX_TEXT_READ_LENGHT)];
|
||||
if !text_buf.is_empty() {
|
||||
file.read_exact(&mut text_buf)
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&file_path_full_path, e)))?;
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.await
|
||||
.map_err(|e| FileIOError::from((&file_path_full_path, e)))?;
|
||||
}
|
||||
|
||||
let charset = is_text(&text_buf, text_buf.len() == (content_lenght as usize)).unwrap_or("");
|
||||
|
||||
// Only browser recognized types, everything else should be text/plain
|
||||
// https://www.iana.org/assignments/media-types/media-types.xhtml#table-text
|
||||
let mime_type = match extension {
|
||||
// HyperText Markup Language
|
||||
"html" | "htm" => "text/html",
|
||||
// Cascading Style Sheets
|
||||
"css" => "text/css",
|
||||
// Javascript
|
||||
"js" | "mjs" => "text/javascript",
|
||||
// Comma-separated values
|
||||
"csv" => "text/csv",
|
||||
// Markdown
|
||||
"md" | "markdown" => "text/markdown",
|
||||
// Rich text format
|
||||
"rtf" => "text/rtf",
|
||||
// Web Video Text Tracks
|
||||
"vtt" => "text/vtt",
|
||||
// Extensible Markup Language
|
||||
"xml" => "text/xml",
|
||||
// Text
|
||||
"txt" => "text/plain",
|
||||
_ => {
|
||||
if charset.is_empty() {
|
||||
return Err(HandleCustomUriError::BadRequest(
|
||||
"TODO: This filetype is not supported because of the missing mime type!",
|
||||
));
|
||||
};
|
||||
mime_type
|
||||
}
|
||||
};
|
||||
|
||||
format!("{mime_type}; charset={charset}")
|
||||
} else {
|
||||
mime_type.to_owned()
|
||||
};
|
||||
|
||||
// GET is the only method for which range handling is defined, according to the spec
|
||||
// https://httpwg.org/specs/rfc9110.html#field.range
|
||||
let range = if method == Method::GET {
|
||||
|
|
|
@ -180,18 +180,24 @@ extension_category_enum! {
|
|||
Txt,
|
||||
Rtf,
|
||||
Md,
|
||||
Markdown,
|
||||
}
|
||||
}
|
||||
// config file extensions
|
||||
extension_category_enum! {
|
||||
ConfigExtension _ALL_CONFIG_EXTENSIONS {
|
||||
Ini,
|
||||
Json,
|
||||
Yaml,
|
||||
Yml,
|
||||
Toml,
|
||||
Xml,
|
||||
Mathml,
|
||||
Rss,
|
||||
Csv,
|
||||
Cfg,
|
||||
Compose,
|
||||
Tsconfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,32 +246,96 @@ extension_category_enum! {
|
|||
// code extensions
|
||||
extension_category_enum! {
|
||||
CodeExtension _ALL_CODE_EXTENSIONS {
|
||||
Rs,
|
||||
Ts,
|
||||
Tsx,
|
||||
Js,
|
||||
Jsx,
|
||||
Vue,
|
||||
Php,
|
||||
Py,
|
||||
Rb,
|
||||
// AppleScript
|
||||
Scpt,
|
||||
Scptd,
|
||||
Applescript,
|
||||
// Shell script
|
||||
Sh,
|
||||
Html,
|
||||
Css,
|
||||
Sass,
|
||||
Scss,
|
||||
Less,
|
||||
Bash,
|
||||
Zsh,
|
||||
Fish,
|
||||
Bash,
|
||||
// C, C++
|
||||
C,
|
||||
Cpp,
|
||||
H,
|
||||
Hpp,
|
||||
Java,
|
||||
Scala,
|
||||
Go,
|
||||
// Ruby
|
||||
Rb,
|
||||
// Javascript
|
||||
Js,
|
||||
Mjs,
|
||||
Jsx,
|
||||
// Markup
|
||||
Html,
|
||||
// Stylesheet
|
||||
Css,
|
||||
Sass,
|
||||
Scss,
|
||||
Less,
|
||||
// Crystal
|
||||
Cr,
|
||||
// C#
|
||||
Cs,
|
||||
Csx,
|
||||
D,
|
||||
Dart,
|
||||
// Docker
|
||||
Dockerfile,
|
||||
Go,
|
||||
// Haskell
|
||||
Hs,
|
||||
Java,
|
||||
// Kotlin
|
||||
Kt,
|
||||
Kts,
|
||||
Lua,
|
||||
// Makefile
|
||||
Make,
|
||||
Nim,
|
||||
Nims,
|
||||
// Objective-C
|
||||
M,
|
||||
Mm,
|
||||
// Ocaml
|
||||
Ml,
|
||||
Mli,
|
||||
Mll,
|
||||
Mly,
|
||||
// Perl
|
||||
Pl,
|
||||
// PHP
|
||||
Php,
|
||||
Php1,
|
||||
Php2,
|
||||
Php3,
|
||||
Php4,
|
||||
Php5,
|
||||
Php6,
|
||||
Phps,
|
||||
Phpt,
|
||||
Phtml,
|
||||
// Powershell
|
||||
Ps1,
|
||||
Psd1,
|
||||
Psm1,
|
||||
// Python
|
||||
Py,
|
||||
Qml,
|
||||
R,
|
||||
// Rust
|
||||
Rs,
|
||||
// Solidity
|
||||
Sol,
|
||||
Sql,
|
||||
Swift,
|
||||
// Typescript
|
||||
Ts,
|
||||
Tsx,
|
||||
Vala,
|
||||
Zig,
|
||||
Vue,
|
||||
Scala,
|
||||
Mdx,
|
||||
Astro,
|
||||
Mts,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod extensions;
|
||||
pub mod kind;
|
||||
pub mod magic;
|
||||
pub mod text;
|
||||
|
|
296
crates/file-ext/src/text.rs
Normal file
296
crates/file-ext/src/text.rs
Normal file
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* Based on an excerpt from the File type identification utility by Ian F. Darwin and others
|
||||
* https://github.com/file/file/blob/445f38730df6a2654eadcc180116035cc6788363/src/encoding.c
|
||||
*/
|
||||
|
||||
const F: u8 = 0;
|
||||
const T: u8 = 1;
|
||||
const I: u8 = 2;
|
||||
const X: u8 = 3;
|
||||
|
||||
static TEXT_CHARS: [u8; 256] = [
|
||||
/* BEL BS HT LF VT FF CR */
|
||||
F, F, F, F, F, F, F, T, T, T, T, T, T, T, F, F, /* 0x0X */
|
||||
/* ESC */
|
||||
F, F, F, F, F, F, F, F, F, F, F, T, F, F, F, F, /* 0x1X */
|
||||
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x2X */
|
||||
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x3X */
|
||||
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x4X */
|
||||
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x5X */
|
||||
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x6X */
|
||||
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, F, /* 0x7X */
|
||||
/* NEL */
|
||||
X, X, X, X, X, T, X, X, X, X, X, X, X, X, X, X, /* 0x8X */
|
||||
X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, /* 0x9X */
|
||||
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xaX */
|
||||
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xbX */
|
||||
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xcX */
|
||||
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xdX */
|
||||
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xeX */
|
||||
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xfX */
|
||||
];
|
||||
|
||||
fn looks_latin1(buf: &[u8]) -> bool {
|
||||
buf.iter().all(|&byte| byte == T || byte == I)
|
||||
}
|
||||
|
||||
const XX: u8 = 0xF1; // invalid: size 1
|
||||
const AS: u8 = 0xF0; // ASCII: size 1
|
||||
const S1: u8 = 0x02; // accept 0, size 2
|
||||
const S2: u8 = 0x13; // accept 1, size 3
|
||||
const S3: u8 = 0x03; // accept 0, size 3
|
||||
const S4: u8 = 0x23; // accept 2, size 3
|
||||
const S5: u8 = 0x34; // accept 3, size 4
|
||||
const S6: u8 = 0x04; // accept 0, size 4
|
||||
const S7: u8 = 0x44; // accept 4, size 4
|
||||
const LOCB: u8 = 0x80;
|
||||
const HICB: u8 = 0xBF;
|
||||
|
||||
static FIRST: [u8; 256] = [
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x00-0x0F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x10-0x1F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x20-0x2F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x30-0x3F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x40-0x4F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x50-0x5F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x60-0x6F
|
||||
AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, AS, // 0x70-0x7F
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, // 0x80-0x8F
|
||||
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, // 0x90-0x9F
|
||||
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, // 0xA0-0xAF
|
||||
XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, // 0xB0-0xBF
|
||||
XX, XX, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, // 0xC0-0xCF
|
||||
S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, S1, // 0xD0-0xDF
|
||||
S2, S3, S3, S3, S3, S3, S3, S3, S3, S3, S3, S3, S3, S4, S3, S3, // 0xE0-0xEF
|
||||
S5, S6, S6, S6, S7, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, // 0xF0-0xFF
|
||||
];
|
||||
|
||||
struct AcceptRange {
|
||||
lo: u8,
|
||||
hi: u8,
|
||||
}
|
||||
|
||||
static EMPTY_ACCEPT_RANGE: AcceptRange = AcceptRange { lo: 0, hi: 0 };
|
||||
|
||||
static ACCEPT_RANGES: [AcceptRange; 5] = [
|
||||
AcceptRange { lo: LOCB, hi: HICB },
|
||||
AcceptRange { lo: 0xA0, hi: HICB },
|
||||
AcceptRange { lo: LOCB, hi: 0x9F },
|
||||
AcceptRange { lo: 0x90, hi: HICB },
|
||||
AcceptRange { lo: LOCB, hi: 0x8F },
|
||||
];
|
||||
|
||||
fn looks_utf8(buf: &[u8], partial: bool) -> bool {
|
||||
let mut ctrl = false;
|
||||
|
||||
let mut it = buf.iter();
|
||||
'outer: while let Some(byte) = it.next() {
|
||||
/* 0xxxxxxx is plain ASCII */
|
||||
if (byte & 0x80) == 0 {
|
||||
/*
|
||||
* Even if the whole file is valid UTF-8 sequences,
|
||||
* still reject it if it uses weird control characters.
|
||||
*/
|
||||
|
||||
if TEXT_CHARS[(*byte) as usize] != T {
|
||||
ctrl = true
|
||||
}
|
||||
/* 10xxxxxx never 1st byte */
|
||||
} else if (byte & 0x40) == 0 {
|
||||
return false;
|
||||
/* 11xxxxxx begins UTF-8 */
|
||||
} else {
|
||||
let x = FIRST[(*byte) as usize];
|
||||
if x == XX {
|
||||
return false;
|
||||
}
|
||||
|
||||
let following = if (byte & 0x20) == 0 {
|
||||
/* 110xxxxx */
|
||||
1
|
||||
} else if (byte & 0x10) == 0 {
|
||||
/* 1110xxxx */
|
||||
2
|
||||
} else if (byte & 0x08) == 0 {
|
||||
/* 11110xxx */
|
||||
3
|
||||
} else if (byte & 0x04) == 0 {
|
||||
/* 111110xx */
|
||||
4
|
||||
} else if (byte & 0x02) == 0 {
|
||||
/* 1111110x */
|
||||
5
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let accept_range = ACCEPT_RANGES
|
||||
.get((x >> 4) as usize)
|
||||
.unwrap_or(&EMPTY_ACCEPT_RANGE);
|
||||
for n in 0..following {
|
||||
let Some(&following_byte) = it.next() else {
|
||||
break 'outer;
|
||||
};
|
||||
|
||||
if n == 0 && (following_byte < accept_range.lo || following_byte > accept_range.hi)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (following_byte & 0x80) == 0 || (following_byte & 0x40) != 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial || !ctrl
|
||||
}
|
||||
|
||||
fn looks_utf8_with_bom(buf: &[u8], partial: bool) -> bool {
|
||||
if buf.len() > 3 && buf[0] == 0xef && buf[1] == 0xbb && buf[2] == 0xbf {
|
||||
looks_utf8(&buf[3..], partial)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
enum UCS16 {
|
||||
BigEnd,
|
||||
LittleEnd,
|
||||
}
|
||||
|
||||
fn looks_ucs16(buf: &[u8]) -> Option<UCS16> {
|
||||
if buf.len() % 2 == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bigend = if buf[0] == 0xff && buf[1] == 0xfe {
|
||||
false
|
||||
} else if buf[0] == 0xfe && buf[1] == 0xff {
|
||||
true
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut hi: u32 = 0;
|
||||
for chunck in buf[2..].chunks_exact(2) {
|
||||
let mut uc = (if bigend {
|
||||
(chunck[1] as u32) | (chunck[0] as u32) << 8
|
||||
} else {
|
||||
(chunck[0] as u32) | (chunck[1] as u32) << 8
|
||||
}) & 0xffff;
|
||||
|
||||
match uc {
|
||||
0xfffe | 0xffff => return None,
|
||||
// UCS16_NOCHAR
|
||||
_ if (0xfdd0..=0xfdef).contains(&uc) => return None,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if hi != 0 {
|
||||
// UCS16_LOSURR
|
||||
if (0xdc00..=0xdfff).contains(&uc) {
|
||||
return None;
|
||||
}
|
||||
uc = 0x10000 + 0x400 * (hi - 1) + (uc - 0xdc00);
|
||||
hi = 0;
|
||||
}
|
||||
|
||||
if uc < 128 && TEXT_CHARS[uc as usize] != T {
|
||||
return None;
|
||||
}
|
||||
|
||||
// UCS16_HISURR
|
||||
if (0xd800..=0xdbff).contains(&uc) {
|
||||
hi = uc - 0xd800 + 1;
|
||||
}
|
||||
|
||||
// UCS16_LOSURR
|
||||
if (0xdc00..=0xdfff).contains(&uc) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(if bigend {
|
||||
UCS16::BigEnd
|
||||
} else {
|
||||
UCS16::LittleEnd
|
||||
})
|
||||
}
|
||||
|
||||
enum UCS32 {
|
||||
BigEnd,
|
||||
LittleEnd,
|
||||
}
|
||||
|
||||
fn looks_ucs32(buf: &[u8]) -> Option<UCS32> {
|
||||
if buf.len() % 4 == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bigend = if buf[0] == 0xff && buf[1] == 0xfe && buf[2] == 0 && buf[3] == 0 {
|
||||
false
|
||||
} else if buf[0] == 0 && buf[1] == 0 && buf[2] == 0xfe && buf[3] == 0xff {
|
||||
true
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
for chunck in buf[4..].chunks_exact(4) {
|
||||
let uc: u32 = if bigend {
|
||||
(chunck[3] as u32)
|
||||
| (chunck[2] as u32) << 8
|
||||
| (chunck[1] as u32) << 16
|
||||
| (chunck[0] as u32) << 24
|
||||
} else {
|
||||
(chunck[0] as u32)
|
||||
| (chunck[1] as u32) << 8
|
||||
| (chunck[2] as u32) << 16
|
||||
| (chunck[3] as u32) << 24
|
||||
};
|
||||
|
||||
if uc == 0xfffe {
|
||||
return None;
|
||||
}
|
||||
if uc < 128 && TEXT_CHARS[uc as usize] != T {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(if bigend {
|
||||
UCS32::BigEnd
|
||||
} else {
|
||||
UCS32::LittleEnd
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_text(data: &[u8], partial: bool) -> Option<&'static str> {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if looks_utf8_with_bom(data, partial) || looks_utf8(data, partial) {
|
||||
return Some("utf-8");
|
||||
}
|
||||
|
||||
match looks_ucs16(data) {
|
||||
Some(UCS16::BigEnd) => return Some("utf-16be"),
|
||||
Some(UCS16::LittleEnd) => return Some("utf-16le"),
|
||||
None => (),
|
||||
}
|
||||
|
||||
match looks_ucs32(data) {
|
||||
Some(UCS32::BigEnd) => return Some("utf-32be"),
|
||||
Some(UCS32::LittleEnd) => return Some("utf-32le"),
|
||||
None => (),
|
||||
}
|
||||
|
||||
if looks_latin1(data) {
|
||||
Some("iso-8859-1")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -4,7 +4,11 @@
|
|||
linear-gradient(45deg, transparent 75%, #16161b 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #16161b 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
}
|
||||
|
||||
.checkersLight {
|
||||
|
@ -13,5 +17,9 @@
|
|||
linear-gradient(45deg, transparent 75%, #e2e2e2 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #e2e2e2 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
useState
|
||||
} from 'react';
|
||||
import { type ExplorerItem, getItemFilePath, useLibraryContext } from '@sd/client';
|
||||
import { PDFViewer, TEXTViewer } from '~/components';
|
||||
import { PDFViewer, TextViewer } from '~/components';
|
||||
import { useCallbackToWatchResize, useIsDark } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { pdfViewerEnabled } from '~/util/pdfViewer';
|
||||
|
@ -181,20 +181,27 @@ export const FileThumb = memo((props: ThumbProps) => {
|
|||
/>
|
||||
);
|
||||
case 'Text':
|
||||
case 'Code':
|
||||
case 'Config':
|
||||
return (
|
||||
<TEXTViewer
|
||||
<TextViewer
|
||||
src={src}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
className={clsx(
|
||||
'h-full w-full px-4 font-mono',
|
||||
'textviewer-scroll h-full w-full overflow-y-auto whitespace-pre-wrap break-words px-4 font-mono',
|
||||
!props.mediaControls
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-auto',
|
||||
className,
|
||||
props.frame && [frameClassName, '!bg-none']
|
||||
)}
|
||||
crossOrigin="anonymous"
|
||||
codeExtension={
|
||||
((itemData.kind === 'Code' ||
|
||||
itemData.kind === 'Config') &&
|
||||
itemData.extension) ||
|
||||
''
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ export function QuickPreview({ transformOrigin }: QuickPreviewProps) {
|
|||
style={styles}
|
||||
className="!pointer-events-none absolute inset-0 z-50 grid h-screen place-items-center"
|
||||
>
|
||||
<div className="!pointer-events-auto flex h-5/6 max-h-screen w-11/12 flex-col rounded-md border border-app-line bg-app-box text-ink shadow-app-shade">
|
||||
<div className="!pointer-events-auto flex h-5/6 max-h-screen w-11/12 flex-col overflow-y-auto rounded-md border border-app-line bg-app-box text-ink shadow-app-shade">
|
||||
<nav className="relative flex w-full flex-row">
|
||||
<Dialog.Close
|
||||
asChild
|
||||
|
|
|
@ -21,7 +21,9 @@ body {
|
|||
inset: 0px;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
mask: linear-gradient(black, black) content-box content-box, linear-gradient(black, black);
|
||||
mask:
|
||||
linear-gradient(black, black) content-box content-box,
|
||||
linear-gradient(black, black);
|
||||
mask-composite: xor;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
@ -54,6 +56,10 @@ body {
|
|||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.explorer-scroll {
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
|
@ -65,10 +71,8 @@ body {
|
|||
&::-webkit-scrollbar-thumb {
|
||||
@apply rounded-[6px] bg-app-box;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.default-scroll {
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
|
@ -154,6 +158,18 @@ body {
|
|||
}
|
||||
}
|
||||
}
|
||||
.textviewer-scroll {
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-app-box;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
|
@ -217,7 +233,9 @@ body {
|
|||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.1),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
import { memo, useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface TEXTViewerProps {
|
||||
src: string;
|
||||
onLoad?: (event: HTMLElementEventMap['load']) => void;
|
||||
onError?: (event: HTMLElementEventMap['error']) => void;
|
||||
className?: string;
|
||||
crossOrigin?: React.ComponentProps<'link'>['crossOrigin'];
|
||||
}
|
||||
|
||||
export const TEXTViewer = memo(
|
||||
({ src, onLoad, onError, className, crossOrigin }: TEXTViewerProps) => {
|
||||
// Ignore empty urls
|
||||
const href = !src || src === '#' ? null : src;
|
||||
const [quickPreviewContent, setQuickPreviewContent] = useState('');
|
||||
|
||||
// Use link preload as a hack to get access to an onLoad and onError events for the object tag
|
||||
// as well as to normalize the URL
|
||||
const link = useMemo(() => {
|
||||
if (href == null) return null;
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.as = 'fetch';
|
||||
link.rel = 'preload';
|
||||
if (crossOrigin) link.crossOrigin = crossOrigin;
|
||||
link.href = href;
|
||||
|
||||
link.addEventListener('load', () => link.remove());
|
||||
link.addEventListener('error', () => link.remove());
|
||||
|
||||
return link;
|
||||
}, [crossOrigin, href]);
|
||||
|
||||
// The useLayoutEffect is used to ensure that the event listeners are added before the object is loaded
|
||||
// The useLayoutEffect declaration order is important here
|
||||
useLayoutEffect(() => {
|
||||
if (!link) return;
|
||||
|
||||
if (onLoad) link.addEventListener('load', onLoad);
|
||||
if (onError) link.addEventListener('error', onError);
|
||||
|
||||
return () => {
|
||||
if (onLoad) link.removeEventListener('load', onLoad);
|
||||
if (onError) link.removeEventListener('error', onError);
|
||||
};
|
||||
}, [link, onLoad, onError]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!link) return;
|
||||
document.head.appendChild(link);
|
||||
|
||||
const loadContent = async () => {
|
||||
if (link.href) {
|
||||
const response = await fetch(link.href);
|
||||
if (response.ok) {
|
||||
response.text().then((text) => setQuickPreviewContent(text));
|
||||
}
|
||||
}
|
||||
};
|
||||
loadContent();
|
||||
|
||||
return () => link.remove();
|
||||
}, [link]);
|
||||
|
||||
// Use link to normalize URL
|
||||
return link ? (
|
||||
<pre
|
||||
className={className}
|
||||
style={{ wordWrap: 'break-word', whiteSpace: 'pre-wrap', colorScheme: 'dark' }}
|
||||
>
|
||||
{quickPreviewContent}
|
||||
</pre>
|
||||
) : null;
|
||||
}
|
||||
);
|
101
interface/components/TextViewer/index.tsx
Normal file
101
interface/components/TextViewer/index.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import clsx from 'clsx';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import './prism.css';
|
||||
|
||||
export interface TextViewerProps {
|
||||
src: string;
|
||||
onLoad?: (event: HTMLElementEventMap['load']) => void;
|
||||
onError?: (event: HTMLElementEventMap['error']) => void;
|
||||
className?: string;
|
||||
codeExtension?: string;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
type Worker = typeof import('./worker')
|
||||
export const worker = new ComlinkWorker<Worker>(new URL('./worker', import.meta.url));
|
||||
|
||||
const NEW_LINE_EXP = /\n(?!$)/g;
|
||||
|
||||
export const TextViewer = memo(
|
||||
({ src, onLoad, onError, className, codeExtension }: TextViewerProps) => {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const [highlight, setHighlight] = useState<{
|
||||
code: string;
|
||||
length: number;
|
||||
language: string;
|
||||
}>();
|
||||
const [textContent, setTextContent] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Ignore empty urls
|
||||
if (!src || src === '#') return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(src, { mode: 'cors', signal: controller.signal })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error(`Invalid response: ${response.statusText}`);
|
||||
const text = await response.text();
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
onLoad?.(new UIEvent('load', {}));
|
||||
setTextContent(text);
|
||||
|
||||
if (codeExtension) {
|
||||
try {
|
||||
const env = await worker.highlight(text, codeExtension);
|
||||
if (env && !controller.signal.aborted) {
|
||||
const match = text.match(NEW_LINE_EXP);
|
||||
setHighlight({
|
||||
...env,
|
||||
length: (match ? match.length + 1 : 1) + 1
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!controller.signal.aborted)
|
||||
onError?.(new ErrorEvent('error', { message: `${error}` }));
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [src, onError, onLoad, codeExtension]);
|
||||
|
||||
return (
|
||||
<pre
|
||||
ref={ref}
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
className,
|
||||
highlight && ['relative !pl-[3.8em]', `language-${highlight.language}`]
|
||||
)}
|
||||
>
|
||||
{highlight ? (
|
||||
<>
|
||||
<span className="pointer-events-none absolute left-0 top-[1em] w-[3em] select-none text-[100%] tracking-[-1px] text-ink-dull">
|
||||
{Array.from(highlight, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={clsx('token block text-end', i % 2 && 'bg-black/40')}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<code
|
||||
style={{ whiteSpace: 'inherit' }}
|
||||
className={clsx('relative', `language-${highlight.language}`)}
|
||||
dangerouslySetInnerHTML={{ __html: highlight.code }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
textContent
|
||||
)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
);
|
143
interface/components/TextViewer/prism.css
Normal file
143
interface/components/TextViewer/prism.css
Normal file
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* atom-dark theme for `prism.js`
|
||||
* Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
|
||||
* @author Joe Gibson (@gibsjose)
|
||||
*/
|
||||
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: #c5c8c6;
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
@apply bg-app-focus;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #7c7c7c;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #c5c8c6;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.keyword,
|
||||
.token.tag {
|
||||
color: #96cbfe;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #ffffb6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.constant {
|
||||
color: #99cc99;
|
||||
}
|
||||
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #ff73fd;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #a8ff60;
|
||||
}
|
||||
|
||||
.token.variable {
|
||||
color: #c6c5fe;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #ededed;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
color: #ffffb6;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: #96cbfe;
|
||||
}
|
||||
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #87c38a;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value {
|
||||
color: #f9ee98;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #dad085;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #e9c062;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #fd971f;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
54
interface/components/TextViewer/prism.ts
Normal file
54
interface/components/TextViewer/prism.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
//@ts-nocheck
|
||||
|
||||
// WARNING: Import order matters
|
||||
|
||||
// Languages
|
||||
// Do not include default ones: markup, html, xml, svg, mathml, ssml, atom, rss, css, clike, javascript, js
|
||||
import 'prismjs/components/prism-applescript.js';
|
||||
import 'prismjs/components/prism-bash.js';
|
||||
import 'prismjs/components/prism-c.js';
|
||||
import 'prismjs/components/prism-cpp.js';
|
||||
import 'prismjs/components/prism-ruby.js';
|
||||
import 'prismjs/components/prism-crystal.js';
|
||||
import 'prismjs/components/prism-csharp.js';
|
||||
import 'prismjs/components/prism-css-extras.js';
|
||||
import 'prismjs/components/prism-csv.js';
|
||||
import 'prismjs/components/prism-d.js';
|
||||
import 'prismjs/components/prism-dart.js';
|
||||
import 'prismjs/components/prism-docker.js';
|
||||
import 'prismjs/components/prism-go-module.js';
|
||||
import 'prismjs/components/prism-go.js';
|
||||
import 'prismjs/components/prism-haskell.js';
|
||||
import 'prismjs/components/prism-ini.js';
|
||||
import 'prismjs/components/prism-java.js';
|
||||
import 'prismjs/components/prism-js-extras.js';
|
||||
import 'prismjs/components/prism-json.js';
|
||||
import 'prismjs/components/prism-jsx.js';
|
||||
import 'prismjs/components/prism-kotlin.js';
|
||||
import 'prismjs/components/prism-less.js';
|
||||
import 'prismjs/components/prism-lua.js';
|
||||
import 'prismjs/components/prism-makefile.js';
|
||||
import 'prismjs/components/prism-markdown.js';
|
||||
import 'prismjs/components/prism-markup-templating.js';
|
||||
import 'prismjs/components/prism-nim.js';
|
||||
import 'prismjs/components/prism-objectivec.js';
|
||||
import 'prismjs/components/prism-ocaml.js';
|
||||
import 'prismjs/components/prism-perl.js';
|
||||
import 'prismjs/components/prism-php.js';
|
||||
import 'prismjs/components/prism-powershell.js';
|
||||
import 'prismjs/components/prism-python.js';
|
||||
import 'prismjs/components/prism-qml.js';
|
||||
import 'prismjs/components/prism-r.js';
|
||||
import 'prismjs/components/prism-rust.js';
|
||||
import 'prismjs/components/prism-sass.js';
|
||||
import 'prismjs/components/prism-scss.js';
|
||||
import 'prismjs/components/prism-solidity.js';
|
||||
import 'prismjs/components/prism-sql.js';
|
||||
import 'prismjs/components/prism-swift.js';
|
||||
import 'prismjs/components/prism-toml.js';
|
||||
import 'prismjs/components/prism-tsx.js';
|
||||
import 'prismjs/components/prism-typescript.js';
|
||||
import 'prismjs/components/prism-typoscript.js';
|
||||
import 'prismjs/components/prism-vala.js';
|
||||
import 'prismjs/components/prism-yaml.js';
|
||||
import 'prismjs/components/prism-zig.js';
|
44
interface/components/TextViewer/worker.ts
Normal file
44
interface/components/TextViewer/worker.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Prism from 'prismjs';
|
||||
import './prism';
|
||||
|
||||
// if you are intending to use Prism functions manually, you will need to set:
|
||||
Prism.manual = true;
|
||||
|
||||
// Mapping between extensions and prismjs language identifier
|
||||
// Only for those that are not already internally resolved by prismjs
|
||||
// https://prismjs.com/#supported-languages
|
||||
const languageMapping = Object.entries({
|
||||
applescript: ['scpt', 'scptd'],
|
||||
// This is not entirely correct, but better than nothing:
|
||||
// https://github.com/PrismJS/prism/issues/3656
|
||||
// https://github.com/PrismJS/prism/issues/3660
|
||||
sh: ['zsh', 'fish'],
|
||||
c: ['h'],
|
||||
cpp: ['hpp'],
|
||||
js: ['mjs'],
|
||||
crystal: ['cr'],
|
||||
cs: ['csx'],
|
||||
makefile: ['make'],
|
||||
nim: ['nims'],
|
||||
objc: ['m', 'mm'],
|
||||
ocaml: ['ml', 'mli', 'mll', 'mly'],
|
||||
perl: ['pl'],
|
||||
php: ['php', 'php1', 'php2', 'php3', 'php4', 'php5', 'php6', 'phps', 'phpt', 'phtml'],
|
||||
powershell: ['ps1', 'psd1', 'psm1'],
|
||||
rust: ['rs']
|
||||
}).reduce<Map<string, string>>((mapping, [id, exts]) => {
|
||||
for (const ext of exts) mapping.set(ext, id);
|
||||
return mapping;
|
||||
}, new Map());
|
||||
|
||||
export const highlight = (code: string, ext: string) => {
|
||||
const language = languageMapping.get(ext) ?? ext;
|
||||
const grammar = Prism.languages[language];
|
||||
|
||||
return grammar
|
||||
? {
|
||||
code: Prism.highlight(code, grammar, language),
|
||||
language
|
||||
}
|
||||
: null;
|
||||
};
|
|
@ -6,7 +6,7 @@ export * from './DragRegion';
|
|||
export * from './Folder';
|
||||
export * from './GridList';
|
||||
export * from './PDFViewer';
|
||||
export * from './TEXTViewer';
|
||||
export * from './TextViewer';
|
||||
export * from './PasswordMeter';
|
||||
export * from './SubtleButton';
|
||||
export * from './TrafficLights';
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"dragselect": "^2.7.4",
|
||||
"framer-motion": "^10.11.5",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
},
|
||||
"types": ["vite-plugin-svgr/client", "vite/client", "node"]
|
||||
"types": ["vite-plugin-comlink/client", "vite-plugin-svgr/client", "vite/client", "node"]
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
"exclude": ["dist"],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
lockfileVersion: '6.1'
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
|
@ -74,6 +74,9 @@ importers:
|
|||
'@tauri-apps/api':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
comlink:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
@ -83,9 +86,6 @@ importers:
|
|||
react-router-dom:
|
||||
specifier: 6.9.0
|
||||
version: 6.9.0(react-dom@18.2.0)(react@18.2.0)
|
||||
vite-plugin-html:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(vite@4.3.9)
|
||||
devDependencies:
|
||||
'@iarna/toml':
|
||||
specifier: ^2.2.5
|
||||
|
@ -123,6 +123,12 @@ importers:
|
|||
vite:
|
||||
specifier: ^4.0.4
|
||||
version: 4.3.9(sass@1.55.0)
|
||||
vite-plugin-comlink:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5(comlink@4.4.1)(vite@4.3.9)
|
||||
vite-plugin-html:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(vite@4.3.9)
|
||||
vite-plugin-svgr:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1(vite@4.3.9)
|
||||
|
@ -700,6 +706,9 @@ importers:
|
|||
phosphor-react:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1(react@18.2.0)
|
||||
prismjs:
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
@ -14275,6 +14284,7 @@ packages:
|
|||
engines: {node: '>= 10.0'}
|
||||
dependencies:
|
||||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
|
@ -14438,6 +14448,7 @@ packages:
|
|||
|
||||
/colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
dev: true
|
||||
|
||||
/combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
|
@ -14445,6 +14456,9 @@ packages:
|
|||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
/comlink@4.4.1:
|
||||
resolution: {integrity: sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==}
|
||||
|
||||
/comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
dev: false
|
||||
|
@ -14574,6 +14588,7 @@ packages:
|
|||
/connect-history-api-fallback@1.6.0:
|
||||
resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: true
|
||||
|
||||
/connect@3.7.0:
|
||||
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
|
||||
|
@ -14588,6 +14603,7 @@ packages:
|
|||
|
||||
/consola@2.15.3:
|
||||
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
||||
dev: true
|
||||
|
||||
/console-control-strings@1.1.0:
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
|
@ -14894,6 +14910,7 @@ packages:
|
|||
domhandler: 4.3.1
|
||||
domutils: 2.8.0
|
||||
nth-check: 2.1.1
|
||||
dev: true
|
||||
|
||||
/css-select@5.1.0:
|
||||
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||
|
@ -15288,6 +15305,7 @@ packages:
|
|||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
entities: 2.2.0
|
||||
dev: true
|
||||
|
||||
/dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
@ -15304,6 +15322,7 @@ packages:
|
|||
engines: {node: '>= 4'}
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
dev: true
|
||||
|
||||
/domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
|
@ -15317,6 +15336,7 @@ packages:
|
|||
dom-serializer: 1.4.1
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
dev: true
|
||||
|
||||
/domutils@3.1.0:
|
||||
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||
|
@ -15330,6 +15350,7 @@ packages:
|
|||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.6.1
|
||||
dev: true
|
||||
|
||||
/dot-prop@4.2.1:
|
||||
resolution: {integrity: sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==}
|
||||
|
@ -15352,6 +15373,7 @@ packages:
|
|||
/dotenv-expand@8.0.3:
|
||||
resolution: {integrity: sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/dotenv@16.0.3:
|
||||
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
|
||||
|
@ -15479,6 +15501,7 @@ packages:
|
|||
hasBin: true
|
||||
dependencies:
|
||||
jake: 10.8.7
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium@1.4.477:
|
||||
resolution: {integrity: sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==}
|
||||
|
@ -15525,6 +15548,7 @@ packages:
|
|||
|
||||
/entities@2.2.0:
|
||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||
dev: true
|
||||
|
||||
/entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
|
@ -16995,6 +17019,7 @@ packages:
|
|||
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
|
||||
dependencies:
|
||||
minimatch: 5.1.6
|
||||
dev: true
|
||||
|
||||
/fill-range@7.0.1:
|
||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
|
||||
|
@ -17224,6 +17249,7 @@ packages:
|
|||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.0
|
||||
dev: true
|
||||
|
||||
/fs-extra@11.1.1:
|
||||
resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
|
||||
|
@ -17960,6 +17986,7 @@ packages:
|
|||
/he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/heap@0.2.7:
|
||||
resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==}
|
||||
|
@ -18025,6 +18052,7 @@ packages:
|
|||
param-case: 3.0.4
|
||||
relateurl: 0.2.7
|
||||
terser: 5.19.2
|
||||
dev: true
|
||||
|
||||
/html-tags@3.3.1:
|
||||
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
|
||||
|
@ -18704,6 +18732,7 @@ packages:
|
|||
chalk: 4.1.2
|
||||
filelist: 1.0.4
|
||||
minimatch: 3.1.2
|
||||
dev: true
|
||||
|
||||
/javascript-natural-sort@0.7.1:
|
||||
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
|
||||
|
@ -19056,6 +19085,12 @@ packages:
|
|||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/json5@2.2.1:
|
||||
resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -21264,6 +21299,7 @@ packages:
|
|||
dependencies:
|
||||
css-select: 4.3.0
|
||||
he: 1.2.0
|
||||
dev: true
|
||||
|
||||
/node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
@ -21659,6 +21695,7 @@ packages:
|
|||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.6.1
|
||||
dev: true
|
||||
|
||||
/parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
|
@ -21801,6 +21838,7 @@ packages:
|
|||
|
||||
/pathe@0.2.0:
|
||||
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
|
||||
dev: true
|
||||
|
||||
/pathe@1.1.1:
|
||||
resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==}
|
||||
|
@ -21962,7 +22000,7 @@ packages:
|
|||
postcss: 8.4.28
|
||||
postcss-value-parser: 4.2.0
|
||||
read-cache: 1.0.0
|
||||
resolve: 1.22.4
|
||||
resolve: 1.22.2
|
||||
|
||||
/postcss-js@4.0.1(postcss@8.4.23):
|
||||
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
|
||||
|
@ -22309,6 +22347,11 @@ packages:
|
|||
resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
/prismjs@1.29.0:
|
||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
|
@ -23491,6 +23534,7 @@ packages:
|
|||
/relateurl@0.2.7:
|
||||
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: true
|
||||
|
||||
/remark-external-links@8.0.0:
|
||||
resolution: {integrity: sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==}
|
||||
|
@ -26268,6 +26312,18 @@ packages:
|
|||
vfile-message: 3.1.4
|
||||
dev: false
|
||||
|
||||
/vite-plugin-comlink@3.0.5(comlink@4.4.1)(vite@4.3.9):
|
||||
resolution: {integrity: sha512-my8BE9GFJEaLc7l3e2SfRUL8JJsN9On8PiW7q4Eyq3g6DHUsNqo5WlS7Butuzc8ngrs24Tf1RC8Xfdda+E5T9w==}
|
||||
peerDependencies:
|
||||
comlink: ^4.3.1
|
||||
vite: '>=2.9.6'
|
||||
dependencies:
|
||||
comlink: 4.4.1
|
||||
json5: 2.2.1
|
||||
magic-string: 0.26.7
|
||||
vite: 4.3.9(sass@1.55.0)
|
||||
dev: true
|
||||
|
||||
/vite-plugin-html@3.2.0(vite@3.2.7):
|
||||
resolution: {integrity: sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==}
|
||||
peerDependencies:
|
||||
|
@ -26306,6 +26362,7 @@ packages:
|
|||
node-html-parser: 5.4.2
|
||||
pathe: 0.2.0
|
||||
vite: 4.3.9(sass@1.55.0)
|
||||
dev: true
|
||||
|
||||
/vite-plugin-svgr@2.2.1(vite@3.2.7):
|
||||
resolution: {integrity: sha512-+EqwahbwjETJH/ssA/66dNYyKN1cO0AStq96MuXmq5maU7AePBMf2lDKfQna49tJZAjtRz+R899BWCsUUP45Fg==}
|
||||
|
|
Loading…
Reference in a new issue