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:
Aditya 2023-08-29 16:17:04 +05:30 committed by GitHub
parent 1184d29379
commit 08ba4f917a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 910 additions and 130 deletions

View file

@ -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/

View file

@ -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'
};

View file

@ -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"
}
}

View file

@ -1,4 +1,5 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-comlink/client" />
declare interface ImportMetaEnv {
VITE_OS: string;

View file

@ -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()]
}
});

View file

@ -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 {

View file

@ -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,

View file

@ -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
View 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
}
}

View file

@ -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;
}

View file

@ -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) ||
''
}
/>
);

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}
);

View 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>
);
}
);

View 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;
}

View 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';

View 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;
};

View file

@ -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';

View file

@ -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",

View file

@ -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"],

View file

@ -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==}