mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05: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:
@@ -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/
|
||||
.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"],
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user