Skip to content

Commit

Permalink
Add match highlighter
Browse files Browse the repository at this point in the history
  • Loading branch information
ShaitanLyss committed Sep 4, 2024
1 parent 1e734ae commit 6efe22e
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@selenite/commons",
"version": "0.23.10",
"version": "0.24.0",
"repository": "github:ShaitanLyss/selenite-commons",
"license": "MIT",
"keywords": [
Expand Down
20 changes: 20 additions & 0 deletions src/lib/components/MatchHighlighter.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { matchingParts } from '$lib/utils';
interface Props {
content?: string;
ref?: string;
caseInsensitive?: boolean;
}
let { content = '', ref = '', caseInsensitive = true }: Props = $props();
const parts = $derived(matchingParts(content, ref, { caseInsensitive }));
</script>

{#each parts as { match, part }}
{#if match}
<mark class="bg-accent text-accent-content outline-accent outline">{part}</mark>
{:else}
{part}
{/if}
{/each}
63 changes: 55 additions & 8 deletions src/lib/utils/string.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, it, expect } from 'vitest';
import * as String from './string';

const avatar =
"I'm blown away\n\
Like a layer of dust on the floor\n\
Of the hall where we first met\n\
Trust was the first thing we lost that day";
describe('string utils', () => {
describe('capitalize', () => {
it('should capitalize strings', () => {
Expand All @@ -12,15 +16,15 @@ describe('string utils', () => {
it('should handle strings with spaces', () => {
expect(String.capitalize('hello world')).toBe('Hello world');
});
})
});
describe('capitalize words', () => {
it('should capitalize words', () => {
expect(String.capitalizeWords('hello world')).toBe('Hello World');
});
it('should handle empty strings', () => {
expect(String.capitalizeWords('')).toBe('');
});
})
});
describe('splitCamelcase', () => {
it('should split camelcase strings', () => {
expect(String.splitCamelCase('helloWorld')).toEqual(['hello', 'World']);
Expand Down Expand Up @@ -72,17 +76,17 @@ describe('string utils', () => {
describe('camelcaseize', () => {
it('should camelcaseize strings', () => {
expect(String.camlelcaseize('hello world')).toBe('helloWorld');
})
});
it('should handle all acronyms', () => {
expect(String.camlelcaseize('VTK')).toBe('vtk');
})
});
it('should handle acronyms', () => {
expect(String.camlelcaseize('VTKMesh')).toBe('vtkMesh');
})
});
it('should handle partial camelcase', () => {
expect(String.camlelcaseize('helloDarling myWorld')).toBe('helloDarlingMyWorld');
})
})
});
});
describe('getSharedWords', () => {
it(`should return empty array on empty strings' words`, () => {
expect(String.getSharedWords([])).toEqual([]);
Expand Down Expand Up @@ -131,4 +135,47 @@ describe('string utils', () => {
).toBe('dogOuts');
});
});

describe('matchingParts', () => {
const matchingParts = String.matchingParts;
it('should return one matching part', () => {
expect(matchingParts('hello world', 'world')).toEqual([
{ part: 'hello ', match: false },
{ part: 'world', match: true }
]);
});
it('should return two matching parts', () => {
expect(matchingParts('world helloworld', 'world')).toEqual([
{ part: 'world', match: true },
{ part: ' hello', match: false },
{ part: 'world', match: true }
]);
});
it('should return one matching part with no case sensitivity', () => {
expect(matchingParts('hello world', 'WORLD', { caseInsensitive: true })).toEqual([
{ part: 'hello ', match: false },
{ part: 'world', match: true }
]);
});
it('should handle multi lines', () => {
expect(matchingParts('hello\nworld', 'world')).toEqual([
{ part: 'hello\n', match: false },
{ part: 'world', match: true }
]);
});
it('should handle no matching parts', () => {
expect(matchingParts('hello world', 'planet')).toEqual([
{ part: 'hello world', match: false }
]);
});
it('should handle no matching parts multiline', () => {
expect(matchingParts('hello\nworld', 'planet')).toEqual([
{ part: 'hello\nworld', match: false }
]);
});
it('should handle avatar', () => {
expect(matchingParts(avatar, 'avatar')).toEqual([{ part: avatar, match: false }]);
expect(matchingParts(avatar, '')).toEqual([{ part: avatar, match: false }]);
});
});
});
32 changes: 32 additions & 0 deletions src/lib/utils/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,35 @@ export function getVarsFromFormatString(formatString: string): string[] {
// return all matches of the regex
return Array.from(formatString.matchAll(/{(\w+).*?}/g)).map((match) => match[1]);
}

export function matchingParts(s: string, ref: string, options: {caseInsensitive?: boolean} = {}): { part: string; match: boolean }[] {
let flags = "s"
if (options.caseInsensitive) {
flags += "i"
}
const re = new RegExp(`(.*?)(${ref})`, flags);

let remainder = s;
const res: ReturnType<typeof matchingParts> = [];
let match: RegExpExecArray | null;

while ((match = re.exec(remainder))) {
if (match[0].length === 0) break;
if (match[1])
res.push({
part: match[1],
match: false
});
res.push({
part: match[2],
match: true
});
remainder = remainder.slice(match[0].length);
}
if (remainder)
res.push({
part: remainder,
match: false
});
return res;
}
10 changes: 9 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
<script lang="ts">
import { page } from '$app/stores';
import '../app.css';
import { capitalizeWords } from '$lib';
let {children} = $props();
const title = $derived(capitalizeWords($page.route.id?.slice(1).split('-').join(' ') ?? ''));
</script>

<main class="p-4 min-h-screen grid justify-center gap-2 items-start place-content-start">
<slot />
{#if title}
<a class="m-auto mb-2" href="/"><btn class="btn w-fit btn-sm">Main Page</btn></a>
<h1 class="text-2xl font-bold m-auto mb-4">{title}</h1>
{/if}
{@render children()}
</main>
2 changes: 1 addition & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// console.log(routes)
</script>

<h1 class="text-2xl font-bold mb-4">Selenite Commons</h1>
<h1 class="text-2xl font-bold my-4">Selenite Commons</h1>

<ul class="menu bg-base-200 rounded-box text-xl">
{#each routes as route}
Expand Down
1 change: 0 additions & 1 deletion src/routes/box-selection/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
let holder = $state<HTMLElement>();
</script>

<h1 class="text-2xl font-bold m-auto mb-4">Box Selection</h1>
<div
use:boxSelection={{
enabled: boxSelectionEnabled,
Expand Down
36 changes: 36 additions & 0 deletions src/routes/match-highlight/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import { isAlphaNumChar, shortcut } from '$lib';
import MatchHighlighter from '$lib/components/MatchHighlighter.svelte';
let ref = $state('dust');
let content = $state(
"I'm blown away\n\
Like a layer of dust on the floor\n\
Of the hall where we first met\n\
Trust was the first thing we lost that day"
);
let refInput = $state<HTMLInputElement>();
</script>

<input
bind:this={refInput}
bind:value={ref}
placeholder="Reference"
class="input input-bordered mb-4"
use:shortcut={{
shortcuts(e) {
if (e.ctrlKey || e.shiftKey || e.altKey) return false;
return isAlphaNumChar(e.key) || e.key === 'Backspace' || e.key === ' ';
},
action: (n, e) => {
if (!refInput) return;
if (e.key === 'Backspace') {
refInput.value = refInput.value.slice(0, -1);
} else refInput.value += e.key;
refInput.focus();
}
}}
onkeydown={(e) => e.key === 'Escape' && refInput?.blur()} />

{#each content.split('\n') as line}
<p class="italic text-lg"><MatchHighlighter content={line} {ref} /></p>
{/each}

0 comments on commit 6efe22e

Please sign in to comment.