This commit is contained in:
Jakob Kordež 2024-06-23 15:52:45 +02:00
parent 933deccca5
commit 0f06cb8b83
22 changed files with 3681 additions and 1310 deletions

View File

@ -3,6 +3,6 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -1,3 +1,4 @@
{
"vite.autoStart": false,
"cSpell.words": ["callsign", "dxcc", "adif", "graphviz", "clublog"]
}

View File

@ -1,38 +1,18 @@
# create-svelte
# Callsign Tester
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
An app for checking the format of a callsign and finding the country it belongs to.
## Creating a project
## Obtaining the Clublog prefix database
If you're seeing this, you've probably already done this step. Congrats!
Read how to obtain the Clublog prefix file [here](https://clublog.freshdesk.com/support/solutions/articles/54902-downloading-the-prefixes-and-exceptions-as-xml)
## Deploying the app
The app can be deployed using the following command:
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
yarn install
yarn build
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
Deploy the contents of the `build` directory to your server.

View File

@ -14,6 +14,7 @@
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^8.56.7",
@ -25,8 +26,9 @@
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.4",

View File

@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
autoprefixer: {}
}
};

View File

@ -1,14 +1,16 @@
import path from 'path';
import { fileURLToPath } from 'url';
const filePath = process.argv[2];
if (!filePath) {
console.error('Please provide a file path as the first argument');
process.exit(1);
}
const outPath = process.argv[3];
if (!outPath) {
console.error('Please provide an output path as the second argument');
process.exit(1);
}
const OUT_DIR = '../src/assets/';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outDir = path.resolve(__dirname, OUT_DIR);
console.log('Outputting to', outDir);
import fs from 'fs';
import { parseStringPromise } from 'xml2js';
@ -21,16 +23,6 @@ const doc: IClublogFile = await parseStringPromise(file);
const now = new Date();
const entities: Map<number, string> = new Map();
for (const entity of doc.clublog.entities[0].entity) {
const id = parseInt(entity.adif[0]);
const name = capitalize(entity.name[0]);
const end = entity.end?.[0];
if (end && new Date(end) < now) continue;
entities.set(id, name);
}
const prefixes: { call: string; entity: number }[] = [];
for (const prefix of doc.clublog.prefixes[0].prefix) {
@ -41,6 +33,7 @@ for (const prefix of doc.clublog.prefixes[0].prefix) {
entity: parseInt(prefix.adif[0])
});
}
console.log('Parsed', prefixes.length, 'prefixes');
// Build the initial trie
import { TrieNode } from '../src/lib/models/trie';
@ -52,13 +45,17 @@ for (const { call, entity } of prefixes) {
// Merge as many nodes as possible
const nodes = new Map([...root.getAllNodes()].map((node) => [node.id, node]));
console.log('Starting merge with', nodes.size, 'nodes');
// Bad merge algorithm, but it works
let anyChanged = true;
while (anyChanged) {
anyChanged = false;
for (const a of nodes.values()) {
if (!nodes.has(a.id)) continue;
for (const b of nodes.values()) {
if (a === b) continue;
if (!nodes.has(b.id)) continue;
if (a.canMerge(b)) {
for (const node of nodes.values()) {
for (const [k, v] of node.children) {
@ -69,13 +66,48 @@ while (anyChanged) {
}
nodes.delete(b.id);
anyChanged = true;
break;
}
}
if (anyChanged) break;
}
}
console.log('Finished merge with', nodes.size, 'nodes');
// Validate the trie
for (const { call, entity } of prefixes) {
if (root.findRaw(call)?.entity !== entity) {
console.error('Failed to find', call, entity);
}
}
// Minimize node IDs
let i = 0;
for (const node of root.getAllNodes()) {
node.id = i++;
}
// Output the trie
const out = [...root.getAllNodes()].map((n) => n.encodeToString()).join('\n');
fs.writeFileSync(outPath, out);
const out = root.encodeToString();
fs.writeFileSync(path.join(outDir, 'dxcc-tree.txt'), out);
// Format entities
import { DxccEntity } from '../src/lib/models/dxcc-entity';
const entities: DxccEntity[] = [];
for (const entity of doc.clublog.entities[0].entity) {
const id = parseInt(entity.adif[0]);
const name = capitalize(entity.name[0]);
const cqz = entity.cqz?.[0];
const cont = entity.cont?.[0];
const end = entity.end?.[0];
if (end && new Date(end) < now) continue;
entities.push({
entity: id,
name,
cqz: cqz ? parseInt(cqz) : undefined,
cont: cont ? cont : undefined
});
}
// Output the entities
fs.writeFileSync(path.join(outDir, 'dxcc-entities.json'), JSON.stringify(entities, null, '\t'));

View File

@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.btn {
@apply bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 active:bg-blue-700;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
let selectionStart: number | null = inputText.length;
let selectionEnd: number | null = inputText.length;
export const generateStyledText: (text: string) => string = (text: string) => text;
export let generateStyledText: (text: string) => string = (text: string) => text;
$: styledText = generateStyledText(inputText) || '&ZeroWidthSpace;';
@ -36,7 +36,7 @@
t.setSelectionRange(selectionStart, selectionEnd);
}
}}
placeholder="Enter a callsign"
placeholder="..."
/>
<div class="styled-text shared" contenteditable="false" bind:innerHTML={styledText} />
</div>
@ -49,17 +49,17 @@
font-size: 32px;
padding: 8px 12px;
text-align: center;
border-radius: 12px;
}
.input {
border: 1px solid #ccc;
border-radius: 10px;
position: absolute;
top: 0;
left: 0;
background: transparent;
color: transparent;
caret-color: black;
caret-color: white;
}
.input::placeholder {
@ -68,6 +68,7 @@
}
.styled-text {
background: #424242;
border: 1px solid transparent;
white-space: pre;
word-wrap: break-word;

108
src/lib/callsign.test.ts Normal file
View File

@ -0,0 +1,108 @@
import { describe, expect, test } from 'vitest';
import { parseCallsign } from './callsign';
describe('parseCallsign', () => {
test('S52KJ', () => {
const data = parseCallsign('S52KJ');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe(null);
expect(data?.suffixPartOf).toBe(null);
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(null);
});
test('s52kj', () => {
const data = parseCallsign('s52kj');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe(null);
expect(data?.suffixPartOf).toBe(null);
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(null);
});
test('S52KJ/P', () => {
const data = parseCallsign('S52KJ/P');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('P');
expect(data?.suffixPartOf).toBe(null);
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(null);
});
test('9A/S52KJ', () => {
const data = parseCallsign('9A/S52KJ');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe('9A');
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe(null);
expect(data?.suffixPartOf).toBe(null);
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(497);
});
test('9A/S52KJ/P', () => {
const data = parseCallsign('9A/S52KJ/P');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe('9A');
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('P');
expect(data?.suffixPartOf).toBe(null);
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(497);
});
test('S52KJ/A', () => {
const data = parseCallsign('S52KJ/A');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('A');
expect(data?.suffixPartOf).toBe(null);
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(null);
});
test('SV1KJ/A', () => {
const data = parseCallsign('SV1KJ/A');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('SV');
expect(data?.baseSuffix).toBe('1KJ');
expect(data?.base).toBe('SV1KJ');
expect(data?.secondarySuffix).toBe('A');
expect(data?.suffixPartOf).toBe('base');
expect(data?.baseDxcc).toBe(180);
expect(data?.prefixDxcc).toBe(null);
});
test('SV/S52KJ/A', () => {
const data = parseCallsign('SV/S52KJ/A');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe('SV');
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('A');
expect(data?.suffixPartOf).toBe('prefix');
expect(data?.baseDxcc).toBe(499);
expect(data?.prefixDxcc).toBe(180);
});
});

View File

@ -1 +1,75 @@
import { findDxcc } from './dxcc-util';
export const callsignPattern = /^([A-Z\d]+\/)?([A-Z\d]+\d+[A-Z]+)(\/[A-Z\d]+)?$/i;
type CallsignData = {
secondaryPrefix: string | null;
basePrefix: string | null;
baseSuffix: string | null;
base: string;
secondarySuffix: string | null;
suffixPartOf: 'base' | 'prefix' | null;
suffixDescription?: string;
baseDxcc: number | null;
prefixDxcc: number | null;
};
export function parseCallsign(callsign: string): CallsignData | null {
callsign = callsign.toUpperCase();
const match = callsign.match(callsignPattern);
if (!match) {
return null;
}
const secondaryPrefix = match[1]?.slice(0, -1) ?? null;
const base = match[2];
const secondarySuffix = match[3]?.slice(1) ?? null;
const baseDxcc = findDxcc(base + '/' + secondarySuffix);
const prefixDxcc = secondaryPrefix ? findDxcc(callsign) : null;
const basePrefix = baseDxcc ? base.slice(0, baseDxcc.prefixLength) : null;
const baseSuffix = baseDxcc ? base.slice(baseDxcc.prefixLength) : null;
const suffixPartOf = prefixDxcc?.withSuffix
? 'prefix'
: !prefixDxcc && baseDxcc?.withSuffix
? 'base'
: null;
return {
secondaryPrefix,
basePrefix,
baseSuffix,
base,
secondarySuffix,
suffixPartOf,
baseDxcc: baseDxcc?.entity || null,
prefixDxcc: prefixDxcc?.entity || null
};
}
export function getSecondarySuffixDescription(callsign: CallsignData): string | null {
if (!callsign.secondarySuffix) {
return null;
}
if (callsign.suffixPartOf === 'base') {
return 'Part of prefix';
}
if (callsign.suffixPartOf === 'prefix') {
return 'Part of secondary prefix';
}
switch (callsign.secondarySuffix) {
case 'P':
return 'Portable';
case 'M':
return 'Mobile';
case 'AM':
return 'Aeronautical mobile';
case 'MM':
return 'Maritime mobile';
}
return null;
}

View File

@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { dxccTree, findDxcc } from './dxcc-util';
import { dxccEntities, dxccTree, findDxcc } from './dxcc-util';
describe('dxccTree', () => {
test('dxccTree is not null', () => {
@ -100,3 +100,17 @@ describe('findDxcc', () => {
expect(result).toBe(null);
});
});
describe('dxccEntities', () => {
test('dxccEntities is not null', () => {
expect(dxccEntities).not.toBe(null);
});
test('499', () => {
const entity = dxccEntities.get(499);
expect(entity).not.toBe(undefined);
expect(entity?.name).toBe('Slovenia');
expect(entity?.cont).toBe('EU');
expect(entity?.cqz).toBe(15);
});
});

View File

@ -1,4 +1,5 @@
import dxccTreeFile from '../assets/dxcc-tree.txt?raw';
import dxccEntitiesFile from '../assets/dxcc-entities.json';
import { TrieNode } from './models/trie';
export const dxccTree = TrieNode.decodeFromString(dxccTreeFile);
@ -38,3 +39,5 @@ export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): Dxc
if (!entity) return null;
return { entity, withSuffix: false, prefixLength };
}
export const dxccEntities = new Map([...dxccEntitiesFile].map((e) => [e.entity, e]));

View File

@ -55,6 +55,10 @@ export class TrieNode {
}
encodeToString(): string {
return [...this.getAllNodes()].map((n) => n._encodeToString()).join('\n');
}
_encodeToString(): string {
const s = [];
if (this.entity) {
s.push(`${this.id}=${this.entity}`);

View File

@ -0,0 +1,40 @@
import { describe, expect, test } from 'vitest';
import { capitalize } from './string-util';
describe('capitalize', () => {
test('Single word', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('Multiple words', () => {
expect(capitalize('hello world')).toBe('Hello World');
});
test('Empty string', () => {
expect(capitalize('')).toBe('');
});
test('Single letter', () => {
expect(capitalize('a')).toBe('A');
});
test('Repeating letters', () => {
expect(capitalize('aaa')).toBe('Aaa');
});
test('Repeating words', () => {
expect(capitalize('hello hello')).toBe('Hello Hello');
});
test('Mixed case', () => {
expect(capitalize('hELLO')).toBe('Hello');
});
test('Mixed case words', () => {
expect(capitalize('hELLO wORLD')).toBe('Hello World');
});
test('Repeating substrings', () => {
expect(capitalize('hihihi hi')).toBe('Hihihi Hi');
});
});

View File

@ -1,12 +1,6 @@
export const capitalize = (s: string) => {
s = s.toLowerCase();
const re = /\b\w+/g;
while (true) {
const result = re.exec(s);
if (!result) break;
s = s.replace(result[0], result[0][0].toUpperCase() + result[0].slice(1));
}
return s;
return s.replace(/\b\w+/g, (word) => word[0].toUpperCase() + word.slice(1));
};
export const rangeToString = (start: string, end: string): string => {

View File

@ -2,4 +2,39 @@
import '../app.css';
</script>
<slot />
<svelte:head>
<title>Callsign Tester</title>
<meta name="description" content="Callsign tester for ham radio operators" />
<meta name="keywords" content="callsign, ham radio, dxcc" />
<meta name="author" content="Jakob Kordež S52KJ" />
</svelte:head>
<div class="flex min-h-screen flex-col bg-[#333] text-[#eee]">
<div class="c flex-1">
<slot />
</div>
<footer class="bg-[#444] py-3">
<div class="c flex justify-between gap-4">
<div>
By <a href="https://jkob.cc">S52KJ</a>
</div>
<div>
Data from <a href="https://clublog.org">ClubLog</a>
</div>
<div>
Source code on <a href="https://github.com/jakobkordez/call-tester">GitHub</a>
</div>
</div>
</footer>
</div>
<style lang="postcss">
.c {
@apply mx-auto w-full max-w-3xl px-6;
}
footer a {
@apply text-[#bbe];
}
</style>

View File

@ -1,30 +1,79 @@
<script lang="ts">
import { callsignPattern } from '$lib/callsign';
import { dxccTree, findDxcc } from '$lib/dxcc-util';
import { getSecondarySuffixDescription, parseCallsign } from '$lib/callsign';
import { dxccEntities } from '$lib/dxcc-util';
import CallsignInput from '../components/callsign-input.svelte';
const baseClass = 'text-red-400';
const prefixClass = 'text-blue-400';
const suffixClass = 'text-green-400';
let callsign = '9a/s52kj/p';
function generateStyledText(text: string): string {
const match = text.match(callsignPattern);
if (!match) return text;
$: callsignData = parseCallsign(callsign);
const [, prefix, base, suffix] = match;
const baseDxcc = findDxcc(base);
const prefixDxcc = prefix ? findDxcc(text) : null;
$: suffixPartOf = [null, 'base', 'prefix'].indexOf(callsignData?.suffixPartOf ?? null);
function styleText(): string {
if (!callsignData) return callsign;
const { base, basePrefix, baseSuffix, secondaryPrefix, secondarySuffix } = callsignData;
// TODO Check if base and prefix same dxcc
const suffClass = [suffixClass, baseClass, prefixClass][suffixPartOf];
return [
prefixDxcc ? `<span class="prefix">${prefix}</span>` : prefix,
`<span class="base">${base}</span>`,
`<span class="dxcc">${base}</span>`,
suffix ? `<span class="suffix">${suffix}</span>` : null
]
.filter(Boolean)
.join('');
secondaryPrefix ? `<span class="${prefixClass}">${secondaryPrefix}/</span>` : '',
basePrefix ? `<span class="${baseClass}">${basePrefix}</span>${baseSuffix}` : base,
secondarySuffix ? `<span class="${suffClass}">/${secondarySuffix}</span>` : ''
].join('');
}
</script>
<div class="mx-auto py-10 max-w-3xl flex flex-col gap-6 px-6">
<h1 class="text-3xl font-medium text-center">Callsign Tester</h1>
<div class="flex flex-col gap-6 py-10">
<h1 class="text-center text-3xl font-medium">Callsign Tester</h1>
<CallsignInput bind:inputText={callsign} {generateStyledText} />
<div>
<div class="text-center">Enter a callsign</div>
<CallsignInput bind:inputText={callsign} generateStyledText={styleText} />
</div>
{#if callsignData}
<div class="flex flex-col gap-4 md:flex-row">
{#if callsignData.prefixDxcc}
<div class="data-box prefix">
<h2 class="text-xl">Secondary prefix</h2>
<div class="font-mono text-2xl font-medium">{callsignData.secondaryPrefix}</div>
<div>{dxccEntities.get(callsignData.prefixDxcc)?.name}</div>
</div>
{/if}
{#if callsignData.baseDxcc}
<div class="data-box base">
<h2 class="text-xl">Prefix</h2>
<div class="font-mono text-2xl font-medium">{callsignData.basePrefix}</div>
<div>{dxccEntities.get(callsignData.baseDxcc)?.name}</div>
</div>
{/if}
{#if callsignData.secondarySuffix}
<div class={`data-box ${['suffix', 'base', 'prefix'][suffixPartOf]}`}>
<h2 class="text-xl">Secondary suffix</h2>
<div class="font-mono text-2xl font-medium">{callsignData.secondarySuffix}</div>
<div>{getSecondarySuffixDescription(callsignData) ?? ''}</div>
</div>
{/if}
</div>
{/if}
</div>
<style lang="postcss">
.data-box {
@apply flex-1 rounded-xl p-4;
}
.data-box.prefix {
@apply bg-blue-700/40;
}
.data-box.base {
@apply bg-red-700/40;
}
.data-box.suffix {
@apply bg-green-700/40;
}
</style>

View File

@ -1,4 +1,4 @@
import adapter from '@sveltejs/adapter-auto';
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
@ -11,7 +11,9 @@ const config = {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
adapter: adapter({
fallback: 'index.html'
})
}
};

View File

@ -2,8 +2,7 @@
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
extend: {}
},
plugins: [],
}
plugins: []
};

View File

@ -360,6 +360,11 @@
dependencies:
import-meta-resolve "^4.1.0"
"@sveltejs/adapter-static@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz#b505c429616c3319d40d293b741f6915da143f49"
integrity sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==
"@sveltejs/kit@^2.0.0":
version "2.5.17"
resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-2.5.17.tgz#e59e00be7a86021c897ce65a540740473535898e"
@ -422,9 +427,9 @@
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/node@*", "@types/node@^20.14.6":
version "20.14.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.6.tgz#f3c19ffc98c2220e18de259bb172dd4d892a6075"
integrity sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==
version "20.14.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.8.tgz#45c26a2a5de26c3534a9504530ddb3b27ce031ac"
integrity sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==
dependencies:
undici-types "~5.26.4"
@ -920,9 +925,9 @@ eastasianwidth@^0.2.0:
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
electron-to-chromium@^1.4.796:
version "1.4.807"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz#4d6c5ea1516f0164ac5bfd487ccd4ee9507c8f01"
integrity sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==
version "1.4.810"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz#7dee01b090b9e048e6db752f7b30921790230654"
integrity sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==
emoji-regex@^8.0.0:
version "8.0.0"
@ -1930,7 +1935,12 @@ prettier-plugin-svelte@^3.1.2:
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.5.tgz#ec004ed6626f59655cfd3fe88154c7cf41c90a2e"
integrity sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==
prettier@^3.1.1:
prettier-plugin-tailwindcss@^0.6.5:
version "0.6.5"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz#e05202784a3f41889711ae38c75c5b8cad72f368"
integrity sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==
prettier@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
@ -2124,16 +2134,8 @@ std-env@^3.5.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -2151,14 +2153,7 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -2401,9 +2396,9 @@ typescript-eslint@^8.0.0-alpha.20:
"@typescript-eslint/utils" "8.0.0-alpha.30"
typescript@^5.0.0, typescript@^5.0.3:
version "5.4.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
version "5.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507"
integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==
ufo@^1.5.3:
version "1.5.3"