├── src
├── client
│ ├── index.ts
│ └── client.ts
├── reset.d.ts
├── index.ts
├── components
│ ├── icons
│ │ ├── index.ts
│ │ ├── settings.tsx
│ │ └── nodejs.tsx
│ ├── preact-env.d.ts
│ ├── ModalCloseButton.scss
│ ├── ModalTitle.tsx
│ ├── ModalOverlay.scss
│ ├── SuggestionPanel.scss
│ ├── ModalCloseButton.tsx
│ ├── ModelHeader.tsx
│ ├── ModalOverlay.tsx
│ ├── Switch.scss
│ ├── ModalBody.tsx
│ ├── ModalContent.tsx
│ ├── Spinner.tsx
│ ├── Switch.tsx
│ ├── DropdownWithInput.scss
│ ├── DropdownWithInput.tsx
│ ├── SuggestionPanel.tsx
│ └── CopilotIcon.tsx
├── i18n
│ ├── index.ts
│ ├── t.spec.ts
│ ├── t.ts
│ ├── en.json
│ └── zh-CN.json
├── patches
│ ├── index.ts
│ ├── typora.ts
│ ├── promise.spec.ts
│ ├── jquery.ts
│ └── promise.ts
├── errors
│ ├── index.ts
│ ├── CommandError.ts
│ ├── NoFreePortError.ts
│ └── PlatformError.ts
├── logging.ts
├── utils
│ ├── tools.proof.ts
│ ├── random.ts
│ ├── function.ts
│ ├── observable.ts
│ ├── dom.ts
│ ├── diff.ts
│ ├── cli-tools.ts
│ ├── stream.ts
│ ├── tools.ts
│ ├── lsp.ts
│ └── logging.ts
├── styles.scss
├── constants.ts
├── modules
│ ├── path.spec.ts
│ ├── url.spec.ts
│ └── url.ts
├── footer.scss
├── types
│ └── tools.ts
├── mac-server.ts
├── settings.ts
├── completion.ts
└── footer.tsx
├── docs
├── screenshot.png
├── toolbar-icon.png
├── screenshot.zh-CN.png
└── toolbar-icon.zh-CN.png
├── .githooks
└── commit-msg
├── .vscode
└── settings.json
├── tsconfig.build.json
├── stylelint.config.js
├── test
└── setup.ts
├── .gitignore
├── prettier.config.cjs
├── .editorconfig
├── vitest.config.ts
├── pre-commit.ts
├── tsconfig.json
├── install.ps1
├── LICENSE
├── install.sh
├── .github
└── workflows
│ └── ci.yml
├── rollup.config.ts
├── bin
├── uninstall_linux.sh
├── uninstall_macos.sh
├── uninstall_windows.ps1
├── install_linux.sh
├── install_windows.ps1
└── install_macos.sh
├── commitlint.config.js
├── eslint.config.js
├── package.json
└── README.zh-CN.md
/src/client/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./client";
2 |
--------------------------------------------------------------------------------
/src/reset.d.ts:
--------------------------------------------------------------------------------
1 | import "@total-typescript/ts-reset";
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import "./patches";
2 |
3 | import "./main";
4 |
--------------------------------------------------------------------------------
/src/components/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./nodejs";
2 | export * from "./settings";
3 |
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Snowflyt/typora-copilot/HEAD/docs/screenshot.png
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | export { t, pathOf } from "./t";
2 |
3 | export type { PathOf } from "./t";
4 |
--------------------------------------------------------------------------------
/.githooks/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | npx --no -- commitlint --edit "$1"
3 | npx tsx pre-commit.ts
4 |
--------------------------------------------------------------------------------
/docs/toolbar-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Snowflyt/typora-copilot/HEAD/docs/toolbar-icon.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": ["reqnode"],
3 | "stylelint.validate": ["css", "scss"]
4 | }
5 |
--------------------------------------------------------------------------------
/docs/screenshot.zh-CN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Snowflyt/typora-copilot/HEAD/docs/screenshot.zh-CN.png
--------------------------------------------------------------------------------
/docs/toolbar-icon.zh-CN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Snowflyt/typora-copilot/HEAD/docs/toolbar-icon.zh-CN.png
--------------------------------------------------------------------------------
/src/components/preact-env.d.ts:
--------------------------------------------------------------------------------
1 | type FC
> = import("preact").FunctionalComponent
;
2 |
--------------------------------------------------------------------------------
/src/patches/index.ts:
--------------------------------------------------------------------------------
1 | // Make sure patches to Typora are imported before other patches
2 | import "./typora";
3 |
4 | import "./promise";
5 |
6 | import "./jquery";
7 |
--------------------------------------------------------------------------------
/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export { CommandError } from "./CommandError";
2 | export { NoFreePortError } from "./NoFreePortError";
3 | export { PlatformError } from "./PlatformError";
4 |
--------------------------------------------------------------------------------
/src/components/ModalCloseButton.scss:
--------------------------------------------------------------------------------
1 | .modal-close-button {
2 | font-size: 1.2rem !important;
3 | opacity: 0.5 !important;
4 | }
5 |
6 | .modal-close-button:hover {
7 | opacity: 1 !important;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/ModalTitle.tsx:
--------------------------------------------------------------------------------
1 | const ModalTitle: FC = ({ children }) => {
2 | return {children};
3 | };
4 |
5 | export default ModalTitle;
6 |
--------------------------------------------------------------------------------
/src/errors/CommandError.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Error thrown when a command execution fails.
3 | */
4 | export class CommandError extends Error {
5 | constructor(message: string) {
6 | super(message);
7 | this.name = "CommandError";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/errors/NoFreePortError.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Error thrown when no free port is found.
3 | */
4 | export class NoFreePortError extends Error {
5 | constructor(message: string) {
6 | super(message);
7 | this.name = "NoFreePortError";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/errors/PlatformError.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Error thrown when a platform is not supported.
3 | */
4 | export class PlatformError extends Error {
5 | constructor(message: string) {
6 | super(message);
7 | this.name = "PlatformError";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/logging.ts:
--------------------------------------------------------------------------------
1 | import { createLogger } from "./utils/logging";
2 |
3 | /**
4 | * Logger used across the plugin.
5 | */
6 | export const logger = createLogger({
7 | prefix: `%cCopilot plugin:%c `,
8 | styles: ["font-weight: bold", "font-weight: normal"],
9 | });
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": false,
5 | "outDir": "dist",
6 | "skipLibCheck": true
7 | },
8 | "include": ["src"],
9 | "exclude": ["src/**/*.spec.ts", "src/**/*.proof.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/ModalOverlay.scss:
--------------------------------------------------------------------------------
1 | .modal-overlay {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 | z-index: 99999;
8 | background: rgba(0, 0, 0, 50%);
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | }
13 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /** @satisfies {import("stylelint").Config} */
4 | const config = {
5 | extends: "stylelint-config-standard-scss",
6 | rules: {
7 | "color-function-alias-notation": "with-alpha",
8 | "color-function-notation": "legacy",
9 | },
10 | };
11 |
12 | export default config;
13 |
--------------------------------------------------------------------------------
/src/components/SuggestionPanel.scss:
--------------------------------------------------------------------------------
1 | .suggestion-panel {
2 | position: absolute;
3 | z-index: 9999;
4 | pointer-events: none;
5 | white-space: pre-wrap;
6 | border: 1px solid #ccc;
7 | display: flex;
8 | flex-direction: column;
9 | padding: 0.5em;
10 | border-radius: 5px;
11 | box-shadow: 0 4px 8px rgba(0, 0, 0, 50%);
12 | }
13 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | import { Window } from "happy-dom";
2 |
3 | import "@/patches/typora";
4 |
5 | const window = new Window();
6 | Object.assign(window, {
7 | isWin: false,
8 | dirname: "/usr/share/typora/resources",
9 | _options: {
10 | appLocale: "en",
11 | appVersion: "1.7.6",
12 | },
13 | });
14 | global.window = window as unknown as typeof global.window;
15 |
--------------------------------------------------------------------------------
/src/utils/tools.proof.ts:
--------------------------------------------------------------------------------
1 | import { describe, equal, expect, it } from "typroof";
2 |
3 | import { omit } from "./tools";
4 |
5 | describe("omit", () => {
6 | it("should omit keys from an object", () => {
7 | expect(omit({ a: 1, b: 2, c: 3 }, "a")).to(equal<{ b: number; c: number }>);
8 | expect(omit({ a: 1, b: 2, c: 3 }, "a", "c")).to(equal<{ b: number }>);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/settings.json
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/src/utils/random.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generate a UUID.
3 | * @returns
4 | */
5 | export function generateUUID(): string {
6 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
7 | // eslint-disable-next-line sonarjs/pseudo-random
8 | const r = (Math.random() * 16) | 0;
9 | const v = c === "x" ? r : (r & 0x3) | 0x8;
10 | return v.toString(16);
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | .text-gray {
2 | color: gray !important;
3 | }
4 |
5 | .font-italic {
6 | font-style: italic !important;
7 | }
8 |
9 | .unset-button {
10 | all: unset;
11 | display: inline-block;
12 | cursor: pointer;
13 | text-align: center;
14 | font: inherit;
15 | color: inherit;
16 | background: none;
17 | border: none;
18 | padding: 0;
19 | margin: 0;
20 | box-sizing: border-box;
21 | }
22 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import * as path from "@modules/path";
2 |
3 | import { TYPORA_RESOURCE_DIR } from "./typora-utils";
4 | import { setGlobalVar } from "./utils/tools";
5 |
6 | /**
7 | * Plugin version.
8 | */
9 | export const VERSION = "0.3.11";
10 |
11 | /**
12 | * Copilot plugin directory.
13 | */
14 | export const PLUGIN_DIR = path.join(TYPORA_RESOURCE_DIR, "copilot");
15 | setGlobalVar("__copilotDir", PLUGIN_DIR);
16 |
--------------------------------------------------------------------------------
/src/components/ModalCloseButton.tsx:
--------------------------------------------------------------------------------
1 | import "./ModalCloseButton.scss";
2 |
3 | export interface ModalCloseButtonProps {
4 | onClick?: () => void;
5 | }
6 |
7 | const ModalCloseButton: FC = ({ onClick }) => {
8 | return (
9 |
12 | );
13 | };
14 |
15 | export default ModalCloseButton;
16 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /** @satisfies {import("prettier").Config} */
4 | const config = {
5 | arrowParens: "always",
6 | bracketSameLine: true,
7 | bracketSpacing: true,
8 | experimentalTernaries: true,
9 | plugins: ["prettier-plugin-packagejson"],
10 | printWidth: 100,
11 | semi: true,
12 | singleQuote: false,
13 | tabWidth: 2,
14 | trailingComma: "all",
15 | };
16 |
17 | module.exports = config;
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # Matches multiple files with brace expansion notation
12 | # Set default charset
13 | [*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,css,scss,json}]
14 | charset = utf-8
15 | indent_style = space
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/src/components/ModelHeader.tsx:
--------------------------------------------------------------------------------
1 | const ModalHeader: FC = ({ children }) => {
2 | return (
3 | <>
4 |
11 | {children}
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default ModalHeader;
19 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 |
3 | import { defineConfig } from "vitest/config";
4 |
5 | export default defineConfig({
6 | resolve: {
7 | alias: {
8 | "@modules": path.resolve(__dirname, "src/modules"),
9 | "@": path.resolve(__dirname, "src"),
10 | "@test": path.resolve(__dirname, "test"),
11 | },
12 | },
13 | test: {
14 | setupFiles: ["./test/setup.ts"],
15 | environment: "happy-dom",
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/ModalOverlay.tsx:
--------------------------------------------------------------------------------
1 | import { createPortal } from "preact/compat";
2 | import "./ModalOverlay.scss";
3 |
4 | export interface ModalOverlayProps {
5 | onClose?: () => void;
6 | }
7 |
8 | const ModalOverlay: FC = ({ children, onClose }) => {
9 | return createPortal(
10 |
11 | {children}
12 |
,
13 | document.body,
14 | );
15 | };
16 |
17 | export default ModalOverlay;
18 |
--------------------------------------------------------------------------------
/src/components/Switch.scss:
--------------------------------------------------------------------------------
1 | .switch {
2 | width: 36px;
3 | height: 20px;
4 | border-radius: 20px;
5 | position: relative;
6 | cursor: pointer;
7 | transition: background-color 0.2s;
8 | }
9 |
10 | .switch .toggle {
11 | width: 16px;
12 | height: 16px;
13 | background-color: white;
14 | border-radius: 50%;
15 | position: absolute;
16 | top: 2px;
17 | left: 2px;
18 | transition: left 0.2s;
19 | }
20 |
21 | .switch.on .toggle {
22 | left: 18px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/ModalBody.tsx:
--------------------------------------------------------------------------------
1 | export interface ModalBodyProps {
2 | className?: string;
3 | style?: preact.JSX.CSSProperties;
4 | }
5 |
6 | const ModalBody: FC = ({ children, className, style }) => {
7 | return (
8 | // eslint-disable-next-line @typescript-eslint/no-misused-spread
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | export default ModalBody;
16 |
--------------------------------------------------------------------------------
/src/components/ModalContent.tsx:
--------------------------------------------------------------------------------
1 | const ModalContent: FC = ({ children }) => {
2 | return (
3 | {
13 | e.stopPropagation();
14 | }}>
15 | {children}
16 |
17 | );
18 | };
19 |
20 | export default ModalContent;
21 |
--------------------------------------------------------------------------------
/src/utils/function.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Cache the result of a function.
3 | * @param fn The function to cache.
4 | * @returns
5 | */
6 | export const cache = unknown>(fn: F): F => {
7 | const cache = new Map();
8 | const result = ((...args) => {
9 | const key = JSON.stringify(args);
10 | if (cache.has(key)) return cache.get(key);
11 | const result = fn(...args);
12 | cache.set(key, result);
13 | return result;
14 | }) as F;
15 | Object.defineProperty(result, "name", {
16 | value: fn.name,
17 | writable: false,
18 | enumerable: false,
19 | configurable: true,
20 | });
21 | return result;
22 | };
23 |
--------------------------------------------------------------------------------
/src/utils/observable.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A lightweight observable implementation.
3 | */
4 | export class Observable {
5 | private observers: ((value: T) => void)[] = [];
6 |
7 | subscribe(observer: (value: T) => void): () => void {
8 | this.observers.push(observer);
9 | return () => {
10 | this.observers = this.observers.filter((o) => o !== observer);
11 | };
12 | }
13 |
14 | subscribeOnce(observer: (value: T) => void): void {
15 | const unsubscribe = this.subscribe((value) => {
16 | unsubscribe();
17 | observer(value);
18 | });
19 | }
20 |
21 | next(value: T): void {
22 | this.observers.forEach((observer) => observer(value));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/pre-commit.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 |
3 | import { replaceInFileSync } from "replace-in-file";
4 |
5 | import packageJSON from "./package.json";
6 |
7 | const CONSTANTS_FILE_PATHNAME = "./src/constants.ts";
8 |
9 | const { version } = packageJSON;
10 |
11 | const options = {
12 | files: CONSTANTS_FILE_PATHNAME,
13 | from: /VERSION = ".*"/g,
14 | to: `VERSION = "${version}"`,
15 | };
16 |
17 | if (fs.readFileSync(CONSTANTS_FILE_PATHNAME, "utf-8").includes(options.to)) process.exit(0);
18 |
19 | try {
20 | replaceInFileSync(options);
21 | console.log("Plugin VERSION updated:", version);
22 | } catch (error) {
23 | console.error("Error occurred while updating plugin VERSION:", error);
24 | process.exit(1);
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ES2020",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "Bundler",
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "noEmit": true,
13 |
14 | /* JSX */
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "preact",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "checkJs": true,
21 | "allowUmdGlobalAccess": true,
22 | "noUnusedLocals": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "noUncheckedIndexedAccess": true,
25 |
26 | /* Path aliases */
27 | "baseUrl": ".",
28 | "paths": {
29 | "@/*": ["src/*"],
30 | "@modules/*": ["src/modules/*"],
31 | "@test/*": ["test/*"]
32 | }
33 | },
34 | "include": ["src", "test", "rollup.config.ts", "pre-commit.ts", "vitest.config.ts"]
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/path.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 |
3 | import * as path from "./path";
4 |
5 | describe("basename", () => {
6 | it("should return the last portion of a path", () => {
7 | expect(path.basename("/foo/bar/baz/asdf/quux.html")).toBe("quux.html");
8 | expect(path.basename("/foo/bar/baz/asdf/quux.html", ".html")).toBe("quux");
9 | expect(path.basename("/foo")).toBe("foo");
10 | expect(path.basename("")).toBe("");
11 | expect(path.basename("C:\\")).toBe("C:\\");
12 | });
13 | });
14 |
15 | describe("dirname", () => {
16 | it("should return the directory name of a path", () => {
17 | expect(path.dirname("/foo/bar/baz/asdf/quux")).toBe("/foo/bar/baz/asdf");
18 | expect(path.dirname("/foo/bar/baz/asdf/quux.html")).toBe("/foo/bar/baz/asdf");
19 | expect(path.dirname("/foo/bar/baz/asdf/quux/")).toBe("/foo/bar/baz/asdf");
20 | expect(path.dirname("/foo/bar")).toBe("/foo");
21 | expect(path.dirname("/foo")).toBe("/");
22 | expect(path.dirname("/")).toBe("/");
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | #Requires -RunAsAdministrator
2 | $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/Snowflyt/typora-copilot/releases/latest"
3 | Invoke-WebRequest -Uri $latestRelease.assets[0].browser_download_url -OutFile "typora-copilot-$($latestRelease.tag_name).zip"
4 | If (Test-Path "typora-copilot-$($latestRelease.tag_name)") {
5 | Remove-Item "typora-copilot-$($latestRelease.tag_name)" -Recurse -Force
6 | }
7 | New-Item -ItemType Directory -Path "typora-copilot-$($latestRelease.tag_name)"
8 | Expand-Archive -Path "typora-copilot-$($latestRelease.tag_name).zip" -DestinationPath "typora-copilot-$($latestRelease.tag_name)"
9 | Remove-Item "typora-copilot-$($latestRelease.tag_name).zip"
10 | Set-Location "typora-copilot-$($latestRelease.tag_name)"
11 | Write-Host "Trying to uninstall the previous version (if any)..."
12 | .\bin\uninstall_windows.ps1 -Silent
13 | Write-Host "Trying to install the new version..."
14 | .\bin\install_windows.ps1
15 | Set-Location ..
16 | Remove-Item "typora-copilot-$($latestRelease.tag_name)" -Recurse -Force
17 |
--------------------------------------------------------------------------------
/src/modules/url.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 |
3 | import { fileURLToPath, pathToFileURL } from "./url";
4 |
5 | describe("fileURLToPath", () => {
6 | it("should return a platform-specific path", () => {
7 | const isWinBefore = Files.isWin;
8 |
9 | Files.isWin = true;
10 | expect(fileURLToPath("file:///C:/path/")).toBe("C:\\path\\");
11 | expect(fileURLToPath("file://nas/foo.txt")).toBe("\\\\nas\\foo.txt");
12 | Files.isWin = false;
13 | expect(fileURLToPath("file:///你好.txt")).toBe("/你好.txt");
14 | expect(fileURLToPath("file:///hello world")).toBe("/hello world");
15 |
16 | Files.isWin = isWinBefore;
17 | });
18 | });
19 |
20 | describe("pathToFileURL", () => {
21 | it("should return a file URL object", () => {
22 | expect(pathToFileURL("/foo#1")).toEqual(new URL("file:///foo%231"));
23 | expect(pathToFileURL("/some/path%.c")).toEqual(new URL("file:///some/path%25.c"));
24 | expect(pathToFileURL("C:\\foo\\bar\\test.py")).toEqual(new URL("file:///C:/foo/bar/test.py"));
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Snowflyt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | export interface SpinnerProps {
2 | color?: string;
3 | }
4 |
5 | /**
6 | * An svg spinner.
7 | * @returns
8 | */
9 | const Spinner: FC = ({ color = "gray" }) => {
10 | return (
11 |