Initial commit WIP

This commit is contained in:
Jakob Kordež 2024-06-22 15:02:05 +02:00
commit 933deccca5
34 changed files with 4835 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

3
.vscode/settings.json vendored Normal file
View File

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

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```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
```
## 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.

33
eslint.config.js Normal file
View File

@ -0,0 +1,33 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
];

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "call-tester",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^8.56.7",
"@types/node": "^20.14.6",
"@types/xml2js": "^0.4.14",
"autoprefixer": "^10.4.19",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.4",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0-alpha.20",
"vite": "^5.0.3",
"vitest": "^1.2.0",
"xml2js": "^0.6.2"
},
"type": "module"
}

6
postcss.config.js Normal file
View File

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

81
scripts/clublog-parser.ts Normal file
View File

@ -0,0 +1,81 @@
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);
}
import fs from 'fs';
import { parseStringPromise } from 'xml2js';
import { capitalize } from '../src/lib/string-util';
import { IClublogFile } from '../src/lib/models/clublog';
// Parse the Clublog XML file
const file = fs.readFileSync(filePath, 'utf8');
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) {
const end = prefix.end?.[0];
if (end && new Date(end) < now) continue;
prefixes.push({
call: prefix.call[0],
entity: parseInt(prefix.adif[0])
});
}
// Build the initial trie
import { TrieNode } from '../src/lib/models/trie';
const root = new TrieNode();
for (const { call, entity } of prefixes) {
root.insert(call, entity);
}
// Merge as many nodes as possible
const nodes = new Map([...root.getAllNodes()].map((node) => [node.id, node]));
// Bad merge algorithm, but it works
let anyChanged = true;
while (anyChanged) {
anyChanged = false;
for (const a of nodes.values()) {
for (const b of nodes.values()) {
if (a.canMerge(b)) {
for (const node of nodes.values()) {
for (const [k, v] of node.children) {
if (v === b) {
node.children.set(k, a);
}
}
}
nodes.delete(b.id);
anyChanged = true;
break;
}
}
if (anyChanged) break;
}
}
// Output the trie
const out = [...root.getAllNodes()].map((n) => n.encodeToString()).join('\n');
fs.writeFileSync(outPath, out);

208
scripts/clublog_prefix.py Normal file
View File

@ -0,0 +1,208 @@
import xml.etree.ElementTree as ET
from sys import argv
from collections import defaultdict
import re
root = ET.parse(argv[1]).getroot()
entities = {}
prefixes = []
def capitalize(s: str):
s = s.lower()
for m in re.findall(r"\b\w+", s):
s = s.replace(m, m.capitalize(), 1)
return s
for child in root:
if child.tag == "entities":
for entity in child:
name = adif = None
for prop in entity:
if prop.tag == "name":
name = prop.text
if prop.tag == "adif":
adif = int(prop.text)
entities[adif] = capitalize(name)
if child.tag == "prefixes":
for prefix in child:
call = entity = end = None
for prop in prefix:
if prop.tag == "call":
call = prop.text
if prop.tag == "adif":
entity = int(prop.text)
if prop.tag == "end":
end = prop.text
if end and end < "2024-06-20":
continue
# if entity != 1:
# continue
prefixes.append((call, entity))
# print(len(prefixes), "prefixes found")
# Build trie
class Node:
counter = 0
def __init__(self):
Node.counter += 1
self.id = Node.counter
self.parent: Node = None
self.children: dict[str, Node] = {}
self.entity: int = None
root = Node()
allNodes = []
# Build trie
def insert(node: Node, prefix, entity):
if not prefix:
if node.entity and node.entity != entity:
print(f"Conflict: {node.entity} vs {entity}")
node.entity = entity
return
nextNode = node.children.get(prefix[0])
if not nextNode:
nextNode = Node()
nextNode.parent = node
node.children[prefix[0]] = nextNode
allNodes.append(nextNode)
insert(nextNode, prefix[1:], entity)
for call, entity in prefixes:
insert(root, call, entity)
# Merge nodes
# print("Merge start,", len(allNodes), "nodes")
def canMerge(node: Node, other: Node):
if node.entity != other.entity:
return False
l = set(node.children.keys()).union(other.children.keys())
return all(node.children.get(k) == other.children.get(k) for k in l)
def merge(first: Node, second: Node):
# if first.id > second.id:
# first, second = second, first
for k, c in second.children.items():
c.parent = first
for k, c in second.parent.children.items():
if c == second:
second.parent.children[k] = first
allNodes.remove(second)
anyChanged = True
while anyChanged:
anyChanged = False
i = 0
while i < len(allNodes):
node = allNodes[i]
for other in allNodes:
if node != other and canMerge(node, other):
anyChanged = True
merge(node, other)
break
else:
i += 1
for i in range(len(allNodes)):
for j in range(i + 1, len(allNodes)):
if canMerge(allNodes[i], allNodes[j]):
raise Exception("Merge not completed")
# for n in allNodes:
# print(n.children.items())
# print("Merge done,", len(allNodes), "nodes left")
# Save compiled trie
for node in [root, *allNodes]:
if node.entity:
print(f"{node.id}={node.entity}")
for c in set(node.children.values()):
print(
f"{node.id}-{''.join(sorted(k for k, v in node.children.items() if v == c))}-{c.id}"
)
# Print trie
def rangeToStr(a: str, b: str):
if a == b:
return a
if ord(b) - ord(a) > 1:
return f"{a}-{b}"
return f"{a}{b}"
def toRange(s: list[str]):
s = sorted(s)
ranges = [(s[0], s[0])]
for c in s[1:]:
if ord(c) == ord(ranges[-1][1]) + 1:
ranges[-1] = (ranges[-1][0], c)
else:
ranges.append((c, c))
return "".join(rangeToStr(a, b) for a, b in ranges)
defined = set()
def genGraphvizNode(node: Node):
label = "" if not node.entity else entities[node.entity]
shape = "circle" if not node.entity else "box"
return f' {node.id} [label="{label}" shape="{shape}"];'
def toGraphviz(node: Node, root=True):
ret = []
if node in defined:
return ret
defined.add(node)
ret.append(genGraphvizNode(node))
dd = defaultdict(list)
for k, c in node.children.items():
dd[c.id].append(k.replace("/", "_"))
ret += toGraphviz(c, False)
for k in sorted(dd.keys()):
ret.append(f' {node.id} -> {k} [label="{toRange(dd[k])}"];')
if root:
curr = node
while curr.parent:
ret.append(genGraphvizNode(curr.parent))
label = toRange(k for k, v in curr.parent.children.items() if v == curr)
ret.append(f' {curr.parent.id} -> {curr.id} [label="{label}"];')
curr = curr.parent
ret = "\n".join(["digraph {", *ret, "}"])
return ret
def traverse(s: str, node: Node = root):
if not s:
return node
nextNode = node.children.get(s[0])
if not nextNode:
return None
return traverse(s[1:], nextNode)
# print(toGraphviz(traverse("R")))

50
scripts/graphviz-gen.ts Normal file
View File

@ -0,0 +1,50 @@
const graphPath = process.argv[2];
if (!graphPath) {
console.error('Please provide a graph path as the first argument');
process.exit(1);
}
const prefix = process.argv[3] || '';
import fs from 'fs';
import { TrieNode } from '../src/lib/models/trie';
import { compactChars } from '../src/lib/string-util';
const root = TrieNode.decodeFromString(fs.readFileSync(graphPath, 'utf8'));
const node = root.findRaw(prefix);
if (!node) {
console.error('Prefix not found');
process.exit(1);
}
const handled = new Set<number>();
function printNode(node: TrieNode) {
if (handled.has(node.id)) return;
handled.add(node.id);
const label = node.entity ?? '';
const shape = node.entity ? 'box' : 'circle';
console.log(`${node.id} [label="${label}" shape="${shape}"];`);
const dd = new Map<number, string[]>();
function getDD(id: number): string[] {
if (!dd.has(id)) dd.set(id, []);
return dd.get(id)!;
}
for (const [key, child] of node.children) {
getDD(child.id).push(key);
printNode(child);
}
for (const key of dd.keys()) {
const label = compactChars(dd.get(key)!);
console.log(`${node.id} -> ${key} [label="${label}"];`);
}
}
console.log('digraph G {');
printNode(node);
console.log('}');

7
src/app.css Normal file
View File

@ -0,0 +1,7 @@
@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;
}

13
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

1182
src/assets/dxcc-tree.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
<script lang="ts">
import { onMount } from 'svelte';
export let inputText = '';
let selectionStart: number | null = inputText.length;
let selectionEnd: number | null = inputText.length;
export const generateStyledText: (text: string) => string = (text: string) => text;
$: styledText = generateStyledText(inputText) || '&ZeroWidthSpace;';
onMount(() => {
// Set the initial selection
const input = document.querySelector('.input') as HTMLInputElement;
input.value = inputText;
input.setSelectionRange(selectionStart, selectionEnd);
});
</script>
<div class="relative w-full">
<input
class="input shared"
on:keydown={(e) => {
const t = e.currentTarget;
selectionStart = t.selectionStart;
selectionEnd = t.selectionEnd;
}}
on:input={(e) => {
const t = e.currentTarget;
if (/^[A-Z\d\/]*$/i.test(t.value)) {
inputText = t.value.toUpperCase();
} else {
// Users enter the not supported characters
// Restore the value and selection
t.value = inputText;
t.setSelectionRange(selectionStart, selectionEnd);
}
}}
placeholder="Enter a callsign"
/>
<div class="styled-text shared" contenteditable="false" bind:innerHTML={styledText} />
</div>
<style>
.shared {
font-family: monospace;
text-transform: uppercase;
width: 100%;
font-size: 32px;
padding: 8px 12px;
text-align: center;
}
.input {
border: 1px solid #ccc;
border-radius: 10px;
position: absolute;
top: 0;
left: 0;
background: transparent;
color: transparent;
caret-color: black;
}
.input::placeholder {
font-family: sans-serif;
text-transform: none;
}
.styled-text {
border: 1px solid transparent;
white-space: pre;
word-wrap: break-word;
}
</style>

1
src/lib/callsign.ts Normal file
View File

@ -0,0 +1 @@
export const callsignPattern = /^([A-Z\d]+\/)?([A-Z\d]+\d+[A-Z]+)(\/[A-Z\d]+)?$/i;

102
src/lib/dxcc-util.test.ts Normal file
View File

@ -0,0 +1,102 @@
import { describe, expect, test } from 'vitest';
import { dxccTree, findDxcc } from './dxcc-util';
describe('dxccTree', () => {
test('dxccTree is not null', () => {
expect(dxccTree).not.toBe(null);
});
});
describe('findDxcc', () => {
test('S52KJ', () => {
const result = findDxcc('S52KJ');
expect(result?.entity).toBe(499);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('s52kj', () => {
const result = findDxcc('s52kj');
expect(result?.entity).toBe(499);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('S52KJ/P', () => {
const result = findDxcc('S52KJ/P');
expect(result?.entity).toBe(499);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('SV2AAA', () => {
const result = findDxcc('SV2AAA');
expect(result?.entity).toBe(236);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('SV2AAA/A', () => {
const result = findDxcc('SV2AAA/A');
expect(result?.entity).toBe(180);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(true);
});
test('SV2AAA/AP', () => {
const result = findDxcc('SV2AAA/AP');
expect(result?.entity).toBe(236);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('SV2AAA/P', () => {
const result = findDxcc('SV2AAA/P');
expect(result?.entity).toBe(236);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('SV/S52KJ', () => {
const result = findDxcc('SV/S52KJ');
expect(result?.entity).toBe(236);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('SV/S52KJ/A', () => {
const result = findDxcc('SV/S52KJ/A');
expect(result?.entity).toBe(180);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(true);
});
test('SV/S52KJ/P', () => {
const result = findDxcc('SV/S52KJ/P');
expect(result?.entity).toBe(236);
expect(result?.prefixLength).toBe(2);
expect(result?.withSuffix).toBe(false);
});
test('SV9/S52KJ/A', () => {
const result = findDxcc('SV9/S52KJ/A');
expect(result?.entity).toBe(40);
expect(result?.prefixLength).toBe(3);
expect(result?.withSuffix).toBe(false);
});
test('empty string', () => {
const result = findDxcc('');
expect(result).toBe(null);
});
test('slash only', () => {
const result = findDxcc('/');
expect(result).toBe(null);
});
test('incomplete string', () => {
const result = findDxcc('S');
expect(result).toBe(null);
});
});

40
src/lib/dxcc-util.ts Normal file
View File

@ -0,0 +1,40 @@
import dxccTreeFile from '../assets/dxcc-tree.txt?raw';
import { TrieNode } from './models/trie';
export const dxccTree = TrieNode.decodeFromString(dxccTreeFile);
interface DxccResult {
entity: number;
prefixLength: number;
withSuffix: boolean;
}
export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): DxccResult | null {
prefix = prefix.toUpperCase();
let node = startingNode;
let entity: number | null = null;
let prefixLength = 0;
while (prefix) {
const next = node.children.get(prefix[0]);
if (prefix[0] === '/' || !next) {
const slashPos = prefix.lastIndexOf('/');
if (node.children.has('/') && slashPos > 0) {
const suffix = prefix.slice(slashPos + 1);
const res = findDxcc(suffix, node.children.get('/'));
if (res && res.prefixLength == suffix.length)
return { entity: res.entity, prefixLength, withSuffix: true };
}
break;
}
node = next;
if (node.entity) entity = node.entity;
prefix = prefix.slice(1);
prefixLength++;
}
if (!entity) return null;
return { entity, withSuffix: false, prefixLength };
}

64
src/lib/models/clublog.ts Normal file
View File

@ -0,0 +1,64 @@
interface IEntity {
adif: number;
name: string;
prefix: string;
deleted: boolean;
cqz?: number;
cont?: string;
long?: number;
lat?: number;
start?: string;
end?: string;
whitelist?: boolean;
whitelist_start?: string;
whitelist_end?: string;
}
interface IException {
call: string;
entity: number;
adif: number;
cqz?: number;
cont?: string;
long?: number;
lat?: number;
start?: string;
end?: string;
}
interface IPrefix {
call: string;
entity: number;
adif: number;
cqz?: number;
cont?: string;
long?: number;
lat?: number;
start?: string;
end?: string;
}
interface IInvalidOperation {
call: string;
start?: string;
end?: string;
}
interface IZoneException {
call: string;
zone: number;
start?: string;
end?: string;
}
interface IClublog {
entities: { entity: IEntity[] }[];
exceptions: { exception: IException[] }[];
prefixes: { prefix: IPrefix[] }[];
invalid_operations: { operation: IInvalidOperation[] }[];
zone_exceptions: { exception: IZoneException[] }[];
}
export interface IClublogFile {
clublog: IClublog;
}

View File

@ -0,0 +1,10 @@
export type DxccEntity = {
entity: number;
name: string;
cqz?: number;
cont?: string;
long?: number;
lat?: number;
start?: string;
end?: string;
};

View File

@ -0,0 +1,42 @@
import { describe, expect, test } from 'vitest';
import { TrieNode } from './trie';
describe('parseString', () => {
test('Basic test', () => {
const encoded = `
31-YAP-4
3=401
31-X-3
4=400
`;
const root = TrieNode.decodeFromString(encoded);
// Check root node
expect(root).not.toBe(null);
expect(root.id).toBe(31);
expect(root.children.size).toBe(4);
expect(root.entity).toBe(null);
expect(new Set([...root.children.keys()])).toEqual(new Set(['Y', 'X', 'A', 'P']));
// Check children
const y = root.children.get('Y');
expect(y).not.toBe(null);
const a = root.children.get('A');
expect(a).toBe(y);
const p = root.children.get('P');
expect(p).toBe(y);
expect(y!.id).toBe(4);
expect(y!.entity).toBe(400);
expect(y!.children.size).toBe(0);
const x = root.children.get('X');
expect(x).not.toBe(null);
expect(x).not.toBe(y);
expect(x!.id).toBe(3);
expect(x!.entity).toBe(401);
expect(x!.children.size).toBe(0);
});
});

107
src/lib/models/trie.ts Normal file
View File

@ -0,0 +1,107 @@
let nodeCounter = 0;
export class TrieNode {
public id: number;
constructor(
public children: Map<string, TrieNode> = new Map(),
public entity: number | null = null,
id: number | null = null
) {
if (id) this.id = id;
else this.id = ++nodeCounter;
}
insert(prefix: string, entity: number): void {
if (!prefix) {
if (this.entity && this.entity !== entity)
throw new Error(`Prefix conflict: ${this.entity} vs ${entity}`);
this.entity = entity;
return;
}
let next = this.children.get(prefix[0]);
if (!next) {
next = new TrieNode();
this.children.set(prefix[0], next);
}
next.insert(prefix.slice(1), entity);
}
findRaw(prefix: string): TrieNode | null {
if (!prefix) return this;
const next = this.children.get(prefix[0]);
return next ? next.findRaw(prefix.slice(1)) : null;
}
getAllNodes(): Set<TrieNode> {
const nodes: TrieNode[] = [this];
for (const child of this.children.values()) {
nodes.push(...child.getAllNodes());
}
return new Set(nodes);
}
canMerge(other: TrieNode): boolean {
if (this === other) return false;
if (this.entity !== other.entity) return false;
// Union set of all children keys
const l = new Set([...this.children.keys(), ...other.children.keys()]);
for (const key of l) {
const a = this.children.get(key);
const b = other.children.get(key);
if (a !== b) return false;
}
return true;
}
encodeToString(): string {
const s = [];
if (this.entity) {
s.push(`${this.id}=${this.entity}`);
}
for (const c of new Set(this.children.values())) {
const chars = [];
for (const [k, v] of this.children) {
if (v === c) chars.push(k);
}
chars.sort();
s.push(`${this.id}-${chars.join('')}-${c.id}`);
}
return s.join('\n');
}
static decodeFromString(s: string): TrieNode {
let root: TrieNode | null = null;
const nodes: Map<number, TrieNode> = new Map();
function getNode(id: number): TrieNode {
let node = nodes.get(id);
if (!node) {
node = new TrieNode(undefined, undefined, id);
nodes.set(id, node);
// Assert root is the first node
root ??= node;
}
return node;
}
for (let line of s.trim().split('\n')) {
line = line.trim();
if (!line) continue;
const parts = line.split('=');
if (parts.length === 2) {
const [id, entity] = parts;
const node = getNode(parseInt(id));
node.entity = parseInt(entity);
} else {
const [id, chars, child] = line.split('-');
const parent = getNode(parseInt(id));
const childNode = getNode(parseInt(child));
for (const char of chars) {
parent.children.set(char, childNode);
}
}
}
return root!;
}
}

30
src/lib/string-util.ts Normal file
View File

@ -0,0 +1,30 @@
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;
};
export const rangeToString = (start: string, end: string): string => {
if (start === end) return start;
if (end.charCodeAt(0) - start.charCodeAt(0) === 1) return start + end;
return start + '-' + end;
};
export const compactChars = (s: string[]): string => {
s.sort();
const ranges = [[s[0], s[0]]];
for (let i = 1; i < s.length; i++) {
const last = ranges[ranges.length - 1];
if (s[i].charCodeAt(0) - last[1].charCodeAt(0) === 1) {
last[1] = s[i];
} else {
ranges.push([s[i], s[i]]);
}
}
return ranges.map(([start, end]) => rangeToString(start, end)).join('');
};

View File

@ -0,0 +1,5 @@
<script>
import '../app.css';
</script>
<slot />

1
src/routes/+layout.ts Normal file
View File

@ -0,0 +1 @@
export const ssr = false;

30
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,30 @@
<script lang="ts">
import { callsignPattern } from '$lib/callsign';
import { dxccTree, findDxcc } from '$lib/dxcc-util';
import CallsignInput from '../components/callsign-input.svelte';
let callsign = '9a/s52kj/p';
function generateStyledText(text: string): string {
const match = text.match(callsignPattern);
if (!match) return text;
const [, prefix, base, suffix] = match;
const baseDxcc = findDxcc(base);
const prefixDxcc = prefix ? findDxcc(text) : null;
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('');
}
</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>
<CallsignInput bind:inputText={callsign} {generateStyledText} />
</div>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View File

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// 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()
}
};
export default config;

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

9
vite.config.ts Normal file
View File

@ -0,0 +1,9 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});

2565
yarn.lock Normal file

File diff suppressed because it is too large Load Diff