Compare commits

..

58 commits

Author SHA1 Message Date
bd56e44585
Merge pull request #60 from chris-kruining/renovate/actions-checkout-5.x 2025-08-12 10:37:39 +02:00
renovate[bot]
9cb1984c8f
Update actions/checkout action to v5 2025-08-11 13:38:03 +00:00
02a980eb79
Merge pull request #59 from chris-kruining/renovate/gittools-actions-4.x 2025-08-07 13:51:58 +02:00
renovate[bot]
a238acf0db
Update gittools/actions action to v4.1.0 2025-08-07 11:27:35 +00:00
Chris Kruining
2595c53f83
fix formatting? 2025-07-21 16:20:58 +02:00
Chris Kruining
155b82cbea
update az cli 2025-07-21 16:12:55 +02:00
Chris Kruining
12e3cf2f85
fix dockerfile 2025-07-21 15:41:42 +02:00
Chris Kruining
e59c210ef4
just remove the config file and fall back to gitversion defaults 2025-07-21 15:30:45 +02:00
Chris Kruining
1fec60d46e
remove old input 2025-07-21 15:29:20 +02:00
Chris Kruining
8401ac9593
remove another param 2025-07-21 15:28:17 +02:00
Chris Kruining
d78e75e9c7
remove param 2025-07-21 15:27:12 +02:00
Chris Kruining
28f272b75b
spelling is an art... 2025-07-21 15:26:16 +02:00
Chris Kruining
7a7ceceead
fix gitversion config 2025-07-21 15:24:23 +02:00
Chris Kruining
253ad2e1bc
update gitversion 2025-07-21 15:20:20 +02:00
Chris Kruining
2a238bb835
Merge branch 'main' of https://github.com/chris-kruining/calque 2025-07-21 15:18:25 +02:00
Chris Kruining
1c31ef575a
update deps 2025-07-21 15:18:23 +02:00
f9c65835fd
Merge pull request #58 from chris-kruining/renovate/gittools-actions-4.x 2025-07-09 07:38:01 +02:00
renovate[bot]
8ae594d1f5
Update gittools/actions action to v4.0.1 2025-07-07 23:33:51 +00:00
bda74f01f9
Merge pull request #57 from chris-kruining/renovate/gittools-actions-4.x 2025-07-01 13:20:45 +02:00
renovate[bot]
ddcd451b49
Update gittools/actions action to v4 2025-06-30 10:58:03 +00:00
Chris Kruining
ce5b962e10
update deps 2025-06-23 16:23:17 +02:00
Chris Kruining
c58b597318
replace custom json parser with json.parse as it does preserve the key order after all, it is chrome devtools that sorts them 2025-06-23 16:19:45 +02:00
e7c0a762eb
Merge pull request #56 from chris-kruining/renovate/vitest-monorepo 2025-06-19 09:21:35 +02:00
renovate[bot]
01086b6e9b
Update vitest monorepo to v3.2.4 2025-06-17 20:13:51 +00:00
34eee3d2c1
Merge pull request #55 from chris-kruining/renovate/major-happy-dom-monorepo 2025-06-11 08:30:37 +02:00
renovate[bot]
01b65d337b
Update dependency @happy-dom/global-registrator to v18 2025-06-10 23:05:20 +00:00
c5f9fec9e9
Merge pull request #53 from chris-kruining/renovate/microsoft.resources-resourcegroups-20250401.x 2025-06-10 12:32:43 +02:00
2b749e4c63
Merge pull request #54 from chris-kruining/renovate/vitest-monorepo 2025-06-10 12:32:23 +02:00
renovate[bot]
24da21e722
Update vitest monorepo to v3.2.3 2025-06-09 12:11:48 +00:00
renovate[bot]
db04ee25e4
Update resource Microsoft.Resources/resourceGroups to 2025-04-01 2025-05-28 14:40:34 +00:00
Chris Kruining
c76c016a46
Merge branch 'experiment/keyboard-lock-api' 2025-05-20 15:38:16 +02:00
Chris Kruining
75d349105e
patch the error away 2025-05-20 15:37:41 +02:00
50268b8785
Merge pull request #52 from chris-kruining/renovate/vitest-monorepo 2025-05-20 14:35:29 +02:00
renovate[bot]
2a9ee3525e
Update vitest monorepo to v3.1.4 2025-05-19 18:05:52 +00:00
1a1a15692d
Merge pull request #51 from chris-kruining/renovate/vitest-monorepo 2025-05-06 07:24:45 +02:00
renovate[bot]
95a771051f
Update vitest monorepo to v3.1.3 2025-05-05 19:09:26 +00:00
ca136ebe3b
Merge pull request #47 from chris-kruining/renovate/microsoft.app-containerapps-20250101.x
Update resource Microsoft.App/containerApps to 2025-01-01
2025-04-28 14:21:14 +02:00
a5d82807ce
Merge pull request #48 from chris-kruining/renovate/microsoft.app-managedenvironments-20250101.x
Update resource Microsoft.App/managedEnvironments to 2025-01-01
2025-04-28 14:21:03 +02:00
96ac7c0c77
Merge pull request #49 from chris-kruining/renovate/microsoft.containerregistry-registries-20250401.x
Update resource Microsoft.ContainerRegistry/registries to 2025-04-01
2025-04-28 14:20:46 +02:00
d3e5a8459f
Merge pull request #50 from chris-kruining/renovate/microsoft.resources-resourcegroups-20250301.x
Update resource Microsoft.Resources/resourceGroups to 2025-03-01
2025-04-28 14:20:35 +02:00
08b532106d
Merge pull request #46 from chris-kruining/renovate/gittools-actions-3.x
Update gittools/actions action to v3.2.1
2025-04-28 14:20:24 +02:00
renovate[bot]
651d77c085
Update resource Microsoft.Resources/resourceGroups to 2025-03-01 2025-04-26 19:00:56 +00:00
renovate[bot]
4ff460a7c2
Update resource Microsoft.ContainerRegistry/registries to 2025-04-01 2025-04-26 19:00:53 +00:00
renovate[bot]
9d216dfa89
Update resource Microsoft.App/managedEnvironments to 2025-01-01 2025-04-26 14:01:58 +00:00
renovate[bot]
c1faeb638f
Update resource Microsoft.App/containerApps to 2025-01-01 2025-04-26 14:01:54 +00:00
renovate[bot]
5424854eea
Update gittools/actions action to v3.2.1 2025-04-25 18:06:11 +00:00
a28e20f7a3
Merge pull request #45 from chris-kruining/renovate/vitest-monorepo
Update vitest monorepo to v3.1.2
2025-04-22 07:57:25 +02:00
renovate[bot]
ed7cc9632a
Update vitest monorepo to v3.1.2 2025-04-21 09:51:46 +00:00
4743f9fbb7
Merge pull request #44 from chris-kruining/renovate/gittools-actions-3.x
Update gittools/actions action to v3.2.0
2025-04-03 09:35:28 +00:00
83cffca379
Merge pull request #42 from chris-kruining/renovate/vitest-monorepo
Update vitest monorepo to v3.1.1
2025-04-03 09:35:14 +00:00
renovate[bot]
47bca08a12
Update vitest monorepo to v3.1.1 2025-04-02 06:07:56 +00:00
renovate[bot]
8740ef23cc
Update gittools/actions action to v3.2.0 2025-04-02 06:07:45 +00:00
ed24ac56f3
Merge pull request #43 from chris-kruining/renovate/solid-devtools-0.x
Update dependency solid-devtools to ^0.34.0
2025-04-02 06:03:50 +00:00
renovate[bot]
3bbefc346f
Update dependency solid-devtools to ^0.34.0 2025-04-01 12:22:33 +00:00
64fe5e795e
Merge pull request #41 from chris-kruining/renovate/vitest-monorepo
Update vitest monorepo to v3.0.9
2025-03-18 15:52:07 +01:00
renovate[bot]
8d32b568b8
Update vitest monorepo to v3.0.9 2025-03-17 16:42:55 +00:00
74b35db6c0
Merge pull request #40 from chris-kruining/renovate/vitest-monorepo
Update vitest monorepo to v3.0.8
2025-03-09 12:22:03 +01:00
renovate[bot]
93d1bcce55
Update vitest monorepo to v3.0.8 2025-03-06 18:29:29 +00:00
44 changed files with 1025 additions and 2184 deletions

View file

@ -27,25 +27,23 @@ jobs:
semver: ${{ steps.gitversion.outputs.SemVer }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v3.1.11
uses: gittools/actions/gitversion/setup@v4.1.0
with:
versionSpec: "5.x"
versionSpec: "6.x"
- name: Determine Version
id: gitversion
uses: gittools/actions/gitversion/execute@v3.1.11
with:
useConfigFile: true
uses: gittools/actions/gitversion/execute@v4.1.0
build_and_publish:
name: Build & Publish
runs-on: ubuntu-latest
needs: versionize
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build container images
run: |
@ -73,7 +71,7 @@ jobs:
matrix:
environment: [ 'prd' ]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
sparse-checkout: |
infrastructure
@ -86,13 +84,13 @@ jobs:
subscription-id: ${{ secrets.CALQUE_PRD_SUBSCRIPTION_ID }}
- name: Deploy bicep
uses: Azure/cli@v2
uses: azure/cli@v2
with:
azcliversion: 2.66.0
inlineScript: |
az deployment sub create \
--location westeurope \
--template-file infrastructure/main.bicep \
--parameters infrastructure/params/${{ matrix.environment }}.bicepparam \
--parameters version=${{needs.versionize.outputs.semver}} \
azcliversion: 2.75.0
inlineScript: >-
az deployment sub create
--location westeurope
--template-file infrastructure/main.bicep
--parameters infrastructure/params/${{ matrix.environment }}.bicepparam
--parameters version=${{needs.versionize.outputs.semver}}
--parameters registryUrl=${{ secrets.ACR_LOGIN_SERVER }}

44
.vscode/launch.json vendored
View file

@ -6,7 +6,7 @@
"request": "launch",
"name": "Start dev",
// The path to a JavaScript or TypeScript file to run.
"program": "entry-server.tsx",
"program": "${file}",
// The arguments to pass to the program, if any.
"args": [],
// The working directory of the program.
@ -15,9 +15,40 @@
"env": {},
// If the environment variables should not be inherited from the parent process.
"strictEnv": false,
// If the program should be run in watch mode.
// This is equivalent to passing `--watch` to the `bun` executable.
// You can also set this to "hot" to enable hot reloading using `--hot`.
"watchMode": false,
// If the debugger should stop on the first line of the program.
"stopOnEntry": true,
"stopOnEntry": false,
// If the debugger should be disabled. (for example, breakpoints will not be hit)
"noDebug": false,
// The path to the `bun` executable, defaults to your `PATH` environment variable.
"runtime": "bun",
// The arguments to pass to the `bun` executable, if any.
// Unlike `args`, these are passed to the executable itself, not the program.
"runtimeArgs": [],
},
{
"type": "bun",
"request": "launch",
"name": "Run tests",
// The path to a JavaScript or TypeScript file to run.
"program": "${file}",
// The arguments to pass to the program, if any.
"args": [],
// The working directory of the program.
"cwd": "${workspaceFolder}",
// The environment variables to pass to the program.
"env": {},
// If the environment variables should not be inherited from the parent process.
"strictEnv": false,
// If the program should be run in watch mode.
// This is equivalent to passing `--watch` to the `bun` executable.
// You can also set this to "hot" to enable hot reloading using `--hot`.
"watchMode": false,
// If the debugger should stop on the first line of the program.
"stopOnEntry": false,
// If the debugger should be disabled. (for example, breakpoints will not be hit)
"noDebug": false,
// The path to the `bun` executable, defaults to your `PATH` environment variable.
@ -26,17 +57,16 @@
// Unlike `args`, these are passed to the executable itself, not the program.
"runtimeArgs": [
"--bun",
"--inspect",
"dev"
"test"
],
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"name": "Attach to Bun",
// The URL of the WebSocket inspector to attach to.
// This value can be retreived by using `bun --inspect`.
"url": "ws://localhost:6499/",
"stopOnEntry": true
}
]
}

View file

@ -1,6 +1,10 @@
{
// The path to the `bun` executable.
"bun.runtime": "/path/to/bun",
"bun.debugTerminal.enabled": true,
"bun.debugTerminal.stopOnEntry": true
"bun.debugTerminal": {
// If support for Bun should be added to the default "JavaScript Debug Terminal".
"enabled": true,
// If the debugger should stop on the first line of the program.
"stopOnEntry": false,
}
}

View file

@ -4,16 +4,17 @@ WORKDIR /usr/src/app
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev
COPY patches/ /temp/dev/patches/
RUN cd /temp/dev && bun install --frozen-lockfile
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
COPY patches/ /temp/prod/patches/
RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# RUN echo "SESSION_SECRET=$(head -c 64 /dev/random | base64)" > .env
ENV NODE_ENV=production
ENV SERVER_PRESET=bun

View file

@ -1,20 +0,0 @@
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatchTag
assembly-informational-format: "{InformationalVersion}"
mode: Mainline
tag-prefix: "[vV]"
continuous-delivery-fallback-tag: ci
major-version-bump-message: '\+semver:\s?(breaking|major)'
minor-version-bump-message: '\+semver:\s?(feature|minor)'
patch-version-bump-message: '\+semver:\s?(fix|patch)'
no-bump-message: '\+semver:\s?(none|skip)'
legacy-semver-padding: 4
build-metadata-padding: 4
commits-since-version-source-padding: 4
commit-message-incrementing: Enabled
branches: {}
ignore:
sha: []
increment: Inherit
commit-date-format: yyyy-MM-dd
merge-message-formats: {}

View file

@ -1,18 +1,32 @@
import { defineConfig } from '@solidjs/start/config';
import solidSvg from 'vite-plugin-solid-svg';
import devtools from 'solid-devtools/vite';
import { resolve } from 'node:path';
export default defineConfig({
vite: {
resolve: {
alias: [
{ find: '@', replacement: 'F:\\Github\\calque\\node_modules\\' },
],
},
html: {
cspNonce: 'KAAS_IS_AWESOME',
},
// css: {
// postcss: {
// },
// },
plugins: [
devtools({
autoname: true,
}),
solidSvg(),
{
name: 'temp',
configResolved(config) {
console.log(config.resolve.alias);
},
}
],
},
solid: {

1218
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ param registryUrl string
var appName = 'app'
resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
resource environment 'Microsoft.App/managedEnvironments@2025-01-01' = {
name: 'cea-${context.locationAbbreviation}-${context.environment}-${context.projectName}'
location: context.location
properties: {
@ -29,7 +29,7 @@ resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
}
}
resource app 'Microsoft.App/containerApps@2024-03-01' = {
resource app 'Microsoft.App/containerApps@2025-01-01' = {
name: 'ca-${context.locationAbbreviation}-${context.environment}-${context.projectName}-app'
location: context.location
identity: {

View file

@ -19,7 +19,7 @@ var context = {
deployedAt: deployedAt
}
resource calqueResourceGroup 'Microsoft.Resources/resourceGroups@2024-11-01' = {
resource calqueResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: 'rg-${locationAbbreviation}-${environment}-${projectName}'
location: location
}

View file

@ -4,7 +4,7 @@ targetScope = 'resourceGroup'
param context Context
resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
resource registry 'Microsoft.ContainerRegistry/registries@2025-04-01' = {
name: 'acr${context.locationAbbreviation}${context.environment}${context.projectName}'
location: context.location
sku: {

View file

@ -6,58 +6,50 @@
"bun": ">=1"
},
"dependencies": {
"@solid-primitives/clipboard": "^1.6.0",
"@solid-primitives/context": "^0.3.0",
"@solid-primitives/deep": "^0.3.0",
"@solid-primitives/destructure": "^0.2.0",
"@solid-primitives/event-listener": "^2.4.0",
"@solid-primitives/i18n": "^2.2.0",
"@solid-primitives/scheduled": "^1.5.0",
"@solid-primitives/selection": "^0.1.1",
"@solid-primitives/storage": "^4.3.1",
"@solid-primitives/timer": "^1.4.0",
"@solid-primitives/clipboard": "^1.6.2",
"@solid-primitives/destructure": "^0.2.2",
"@solid-primitives/i18n": "^2.2.1",
"@solid-primitives/scheduled": "^1.5.2",
"@solid-primitives/selection": "^0.1.3",
"@solid-primitives/storage": "^4.3.3",
"@solid-primitives/timer": "^1.4.2",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.1",
"@solidjs/start": "^1.1.7",
"dexie": "^4.0.11",
"flag-icons": "^7.3.2",
"flag-icons": "^7.5.0",
"iterator-helpers-polyfill": "^3.0.1",
"rehype-dom-parse": "^5.0.2",
"rehype-parse": "^9.0.1",
"rehype-remark": "^10.0.0",
"rehype-remark": "^10.0.1",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
"sitemap": "^8.0.0",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.5",
"ts-pattern": "^5.6.2",
"solid-js": "^1.9.7",
"ts-pattern": "^5.7.1",
"unified": "^11.0.5",
"unist-util-ancestor": "^1.4.3",
"unist-util-find": "^3.0.0",
"unist-util-visit": "^5.0.0",
"unist-util-visit-parents": "^6.0.1",
"vinxi": "^0.5.3"
"vinxi": "^0.5.8"
},
"devDependencies": {
"@happy-dom/global-registrator": "^17.1.8",
"@happy-dom/global-registrator": "^18.0.1",
"@sinonjs/fake-timers": "^14.0.0",
"@solidjs/testing-library": "^0.8.10",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/wicg-file-system-access": "^2023.10.5",
"@vitest/browser": "^3.0.7",
"@vitest/coverage-istanbul": "3.0.7",
"@vitest/coverage-v8": "3.0.7",
"bun-types": "^1.2.3",
"jsdom": "^26.0.0",
"playwright": "^1.50.1",
"solid-devtools": "^0.33.0",
"vite-plugin-solid": "^2.11.2",
"@types/wicg-file-system-access": "^2023.10.6",
"@vitest/coverage-istanbul": "3.2.4",
"@vitest/coverage-v8": "3.2.4",
"bun-types": "^1.2.19",
"jsdom": "^26.1.0",
"solid-devtools": "^0.34.3",
"vite-plugin-solid": "^2.11.7",
"vite-plugin-solid-svg": "^0.8.1",
"vitest": "^3.0.7",
"vitest": "^3.2.4",
"workbox-window": "^7.3.0"
},
"scripts": {
@ -67,5 +59,8 @@
"version": "vinxi version",
"test": "vitest --coverage",
"test:ci": "vitest run"
},
"patchedDependencies": {
"@tanstack/directive-functions-plugin@1.119.2": "patches/@tanstack%2Fdirective-functions-plugin@1.119.2.patch"
}
}

View file

@ -0,0 +1,14 @@
diff --git a/dist/esm/index.js b/dist/esm/index.js
index 813fa63450583316c537cadd46db0c6fce055ac7..205b03d0ae77bfe3ee93f42b0e3d4b5f453502ec 100644
--- a/dist/esm/index.js
+++ b/dist/esm/index.js
@@ -13,6 +13,9 @@ function TanStackDirectiveFunctionsPlugin(opts) {
ROOT = config.root;
},
transform(code, id) {
+ if (id.startsWith('/@')) {
+ id = `@/${id.slice(2)}`;
+ }
var _a;
const url = pathToFileURL(id);
url.searchParams.delete("v");

View file

@ -23,6 +23,12 @@
}
}
.search {
position: absolute;
inset-inline-end: 0;
inset-block-start: 0;
}
.suggestions {
position-anchor: --suggestions;

View file

@ -1,12 +1,12 @@
import { createEffect, createSignal, on, onMount } from 'solid-js';
import { Component, createEffect, createMemo, createSignal, For, on, onMount, untrack } from 'solid-js';
import { debounce } from '@solid-primitives/scheduled';
import { createSelection, getTextNodes } from '@solid-primitives/selection';
import { createSource } from '~/features/source';
import { isServer } from 'solid-js/web';
import css from './textarea.module.css';
interface TextareaProps {
class?: string;
title?: string;
value: string;
lang: string;
placeholder?: string;
@ -20,7 +20,7 @@ export function Textarea(props: TextareaProps) {
const [editorRef, setEditorRef] = createSignal<HTMLElement>();
let mounted = false;
const source = createSource(() => props.value);
const source = createSource(props.value);
createEffect(on(() => [props.oninput, source.in] as const, ([oninput, text]) => {
if (!mounted) {
@ -43,8 +43,6 @@ export function Textarea(props: TextareaProps) {
const ref = editorRef();
if (ref) {
console.log(ref.innerHTML);
source.out = ref.innerHTML;
ref.style.height = `1px`;
@ -63,22 +61,21 @@ export function Textarea(props: TextareaProps) {
});
createEffect(() => {
props.oninput?.(source.in);
createHighlights(editorRef()!, 'spelling-error', source.spellingErrors);
});
createEffect(on(() => [editorRef()!, source.spellingErrors] as const, ([ref, errors]) => {
createHighlights(ref, 'spelling-error', errors);
}));
createEffect(() => {
createHighlights(editorRef()!, 'grammar-error', source.grammarErrors);
});
createEffect(on(() => [editorRef()!, source.grammarErrors] as const, ([ref, errors]) => {
createHighlights(ref, 'grammar-error', errors);
}));
createEffect(() => {
createHighlights(editorRef()!, 'search-results', source.queryResults);
});
createEffect(on(() => [editorRef()!, source.queryResults] as const, ([ref, errors]) => {
createHighlights(ref, 'search-results', errors);
}));
return <div
return <>
<Suggestions />
<input class={css.search} type="search" oninput={e => source.query = e.target.value} />
<div
ref={setEditorRef}
class={`${css.textarea} ${props.class}`}
contentEditable
@ -88,7 +85,103 @@ export function Textarea(props: TextareaProps) {
data-placeholder={props.placeholder ?? ''}
on:keydown={e => e.stopPropagation()}
on:pointerdown={e => e.stopPropagation()}
/>;
/>
</>;
}
const Suggestions: Component = () => {
const [selection] = createSelection();
const [suggestionRef, setSuggestionRef] = createSignal<HTMLElement>();
const [suggestions, setSuggestions] = createSignal<string[]>([]);
const marker = createMemo(() => {
if (isServer) {
return;
}
const [n] = selection();
const s = window.getSelection();
if (n === null || s === null || s.rangeCount < 1) {
return;
}
return (findMarkerNode(s.getRangeAt(0)?.commonAncestorContainer) ?? undefined) as HTMLElement | undefined;
});
createEffect<HTMLElement | undefined>((prev) => {
if (prev) {
prev.style.setProperty('anchor-name', null);
}
const m = marker();
const ref = untrack(() => suggestionRef()!);
if (m === undefined) {
if (ref.matches(':popover-open')) {
ref.hidePopover();
}
return;
}
m.style.setProperty('anchor-name', '--suggestions');
if (ref.matches(':not(:popover-open)')) {
ref.showPopover();
}
ref.focus()
return m;
});
createEffect(() => {
marker();
setSuggestions(Array(Math.ceil(Math.random() * 5)).fill('').map((_, i) => `suggestion ${i}`));
});
const onPointerDown = (e: PointerEvent) => {
marker()?.replaceWith(document.createTextNode(e.target.textContent));
};
const onKeyDown = (e: KeyboardEvent) => {
console.log(e);
}
return <menu ref={setSuggestionRef} class={css.suggestions} popover="manual" onkeydown={onKeyDown}>
<For each={suggestions()}>{
suggestion => <li onpointerdown={onPointerDown}>{suggestion}</li>
}</For>
</menu>;
};
const findMarkerNode = (node: Node | null) => {
while (node !== null) {
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).hasAttribute('data-marker')) {
break;
}
node = node.parentNode;
}
return node;
};
const spellChecker = checker(/\w+/gi);
const grammarChecker = checker(/\w+\s+\w+/gi);
function checker(regex: RegExp) {
return (subject: string, lang: string): [number, number][] => {
// return [];
const threshold = .75//.99;
return Array.from<RegExpExecArray>(subject.matchAll(regex)).filter(() => Math.random() >= threshold).map(({ 0: match, index }) => {
return [index, index + match.length] as const;
});
}
}
const createHighlights = (node: Node, type: string, ranges: [number, number][]) => {

View file

@ -1,8 +1,5 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
import { installIntoGlobal } from "iterator-helpers-polyfill";
import 'solid-devtools';
installIntoGlobal();
mount(() => <StartClient />, document.body);

View file

@ -1,6 +1,5 @@
import { trackStore } from "@solid-primitives/deep";
import { Accessor, createEffect, createMemo, untrack } from "solid-js";
import { createStore, unwrap } from "solid-js/store";
import { createStore } from "solid-js/store";
import { CustomPartial } from "solid-js/store/types/store.js";
import { deepCopy, deepDiff, MutarionKind, Mutation } from "~/utilities";
@ -60,7 +59,7 @@ function defaultGroupingFunction<T>(groupBy: keyof T): GroupingFunction<number,
export const createDataSet = <T extends Record<string, any>>(data: Accessor<T[]>, initialOptions?: { sort?: SortOptions<T>, group?: GroupOptions<T> }): DataSet<T> => {
const [state, setState] = createStore<DataSetState<T>>({
value: structuredClone(data()),
value: deepCopy(data()),
snapshot: data(),
sorting: initialOptions?.sort,
grouping: initialOptions?.group,
@ -94,15 +93,12 @@ export const createDataSet = <T extends Record<string, any>>(data: Accessor<T[]>
});
const mutations = createMemo(() => {
trackStore(state.value);
// enumerate all values to make sure the memo is recalculated on any change
Object.values(state.value).map(entry => Object.values(entry ?? {}));
return deepDiff(state.snapshot, state.value).toArray();
});
createEffect(() => {
console.log('muts', mutations());
});
const apply = (data: T[], mutations: Mutation[]) => {
for (const mutation of mutations) {
const path = mutation.key.split('.');

View file

@ -1,38 +0,0 @@
import { describe, expect } from "vitest";
import { it } from "~/test-helpers";
import { createElement, splitBy, mergeNodes } from './ast';
describe('ast', () => {
describe('createElement', () => {
it('should ____', () => {
// Arrange
// Act
// Assert
expect(true).toEqual(true);
});
});
describe('splitBy', () => {
it('should ____', () => {
// Arrange
// Act
// Assert
expect(true).toEqual(true);
});
});
describe('mergeNodes', () => {
it('should ____', () => {
// Arrange
// Act
// Assert
expect(true).toEqual(true);
});
});
});

View file

@ -1,92 +0,0 @@
import type { Node, Text, Parent, RootContent } from 'hast';
import { find } from 'unist-util-find';
import { visit } from 'unist-util-visit';
import { hash } from './hash';
export const createElement = (tagName: string, children: any[], properties: object = {}) => ({ type: 'element', tagName, children, properties });
interface SplitPoint {
node: Text;
offset: number;
}
export const splitBy = (tree: Parent, splitPoints: SplitPoint[]): RootContent[][] => {
const result: RootContent[][] = [];
let remaining: RootContent[] = Object.hasOwn(tree, 'children') ? (tree as Parent).children : [];
let lastNode;
let accumulatedOffset = 0;
for (const { node, offset } of splitPoints) {
if (lastNode !== node) {
accumulatedOffset = 0;
}
const index = remaining.findIndex(c => find(c, n => equals(n, node)));
if (index === -1) {
throw new Error('The tree does not contain the given node');
}
const [targetLeft, targetRight] = splitNode(remaining[index], node, offset - accumulatedOffset);
const left = remaining.slice(0, index);
const right = remaining.slice(index + 1);
if (targetLeft) {
left.push(targetLeft);
}
if (targetRight) {
right.unshift(targetRight);
}
remaining = right;
result.push(left);
lastNode = node;
accumulatedOffset += offset;
}
result.push(remaining);
return result;
};
const splitNode = (node: Node, text: Text, offset: number): [RootContent | undefined, RootContent | undefined] => {
if (offset === 0) {
return [undefined, node as RootContent];
}
if (offset === text.value.length) {
return [node as RootContent, undefined];
}
const left = structuredClone(node) as RootContent;
const right = node as RootContent;
visit(left, (n): n is Text => equals(n, text), n => {
n.value = n.value.slice(0, offset);
})
visit(right, (n): n is Text => equals(n, text), n => {
n.value = n.value.slice(offset);
})
return [left, right];
}
export const mergeNodes = (...nodes: Text[]): Text => {
return { type: 'text', value: nodes.map(n => n.value).join() };
};
const equals = (a: Node, b: Node): boolean => {
if (a === b) {
return true;
}
if (a.type !== b.type) {
return false;
}
return hash(a) === hash(b);
};

View file

@ -1,87 +0,0 @@
import { describe, expect } from "vitest";
import { createEditor } from "./context";
import { render } from "@solidjs/testing-library";
import { it } from "~/test-helpers";
import { createSignal } from "solid-js";
describe('createEditor', () => {
describe('create', () => {
it('should create', async () => {
// Arrange
const res = render(() => <div data-testid="editor" />);
const ref = await res.findByTestId('editor');
// Act
const actual = createEditor(
() => ref,
() => '<p>this is a string</p>'
);
// Assert
expect(actual).toBeTruthy();
});
it('should update after a change has taken place', async () => {
// Arrange
const [value, setValue] = createSignal('<p>this is a string</p>');
const res = render(() => {
const [ref, setRef] = createSignal<Element>();
const [text] = createEditor(ref, value);
return <div ref={setRef} innerHTML={text()} data-testid="editor" />;
});
const ref = await res.findByTestId('editor');
// Act
setValue('<p>this is another totally different string</p>');
// Assert
expect(ref.innerHTML).toBe('<p>this is another totally different string</p>');
});
});
describe('selection', () => {
it('should not fail if there are no selection ranges', async () => {
// Arrange
const res = render(() => {
const [ref, setRef] = createSignal<Element>();
const [text] = createEditor(ref, () => '<p>paragraph 1</p>\n<p>paragraph 2</p>\n<p>paragraph 3</p>');
return <div ref={setRef} innerHTML={text()} data-testid="editor" />;
});
const ref = await res.findByTestId('editor');
// Act
window.getSelection()!.removeAllRanges();
// Assert
expect(true).toBeTruthy();
});
it('should react to changes in selection', async () => {
// Arrange
const res = render(() => {
const [ref, setRef] = createSignal<Element>();
const [text] = createEditor(ref, () => '<p>paragraph 1</p>\n<p>paragraph 2</p>\n<p>paragraph 3</p>');
return <div ref={setRef} innerHTML={text()} data-testid="editor" />;
});
const ref = await res.findByTestId('editor');
// Act
ref.focus();
window.getSelection()!.setBaseAndExtent(ref.childNodes[0].childNodes[0], 0, ref.childNodes[0].childNodes[0], 10);
console.log(window.getSelection()!.rangeCount);
// Assert
expect(true).toBeTruthy();
});
});
});

View file

@ -1,345 +0,0 @@
import { createEventListenerMap, DocumentEventListener, WindowEventListener } from "@solid-primitives/event-listener";
import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount, Setter } from "solid-js";
import { createStore, produce } from "solid-js/store";
import { isServer } from "solid-js/web";
import { createMap } from './map';
import { unified } from "unified";
import rehypeParse from "rehype-parse";
export type SelectFunction = (range: Range) => void;
type Editor = { select: SelectFunction, readonly selection: Accessor<Range | undefined> };
interface EditorStoreType {
text: string;
isComposing: boolean;
selection: Range | undefined;
characterBounds: DOMRect[];
controlBounds: DOMRect;
selectionBounds: DOMRect;
}
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>, setValue: (next: string) => any): Editor {
if (isServer) {
return {
select() { },
selection: () => undefined,
};
}
if (!("EditContext" in window)) {
throw new Error('`EditContext` is not implemented');
}
const [store, setStore] = createStore<EditorStoreType>({
text: value(),
isComposing: false,
selection: undefined,
// Bounds
characterBounds: new Array<DOMRect>(),
controlBounds: new DOMRect(),
selectionBounds: new DOMRect(),
});
const context = new EditContext({
text: store.text,
});
const mutations = observe(ref);
const ast = createMemo(() => parse(store.text));
const indexMap = createMap(ref, ast);
createEffect(() => {
setValue(store.text);
});
// createEffect(() => {
// const selection = store.selection;
// if (!selection) {
// return;
// }
// console.log(indexMap.query(selection));
// });
createEffect(on(() => [ref(), ast()], () => {
console.log('pre rerender?');
const selection = store.selection;
const indices = selection ? indexMap.query(selection) : [];
queueMicrotask(() => {
console.log('post rerender?');
console.log(indices);
});
}));
createEffect(on(value, value => {
if (value !== store.text) {
setStore('text', value);
}
}));
createEffect(on(mutations, ([root, mutations]) => {
const text = (root! as HTMLElement).innerHTML;
if (text !== store.text) {
context.updateText(0, context.text.length, text);
setStore('text', context.text);
}
}));
createEventListenerMap<any>(context, {
textupdate(e: TextUpdateEvent) {
const selection = store.selection;
if (!selection) {
return;
}
selection.extractContents();
selection.insertNode(document.createTextNode(e.text));
selection.collapse();
},
compositionstart() {
setStore('isComposing', true);
},
compositionend() {
setStore('isComposing', false);
},
characterboundsupdate(e: CharacterBoundsUpdateEvent) {
context.updateCharacterBounds(e.rangeStart, []);
},
textformatupdate(e: TextFormatUpdateEvent) {
const formats = e.getTextFormats();
for (const format of formats) {
console.log(format);
}
},
});
function updateControlBounds() {
context.updateControlBounds(ref()!.getBoundingClientRect());
}
function updateSelection(range: Range) {
const [start, end] = indexMap.query(range);
if (!start || !end) {
return;
}
context.updateSelection(start.start + range.startOffset, end.start + range.endOffset);
context.updateSelectionBounds(range.getBoundingClientRect());
setStore('selection', range);
queueMicrotask(() => {
const selection = window.getSelection();
if (selection === null) {
return;
}
if (selection.rangeCount !== 0) {
const existingRange = selection.getRangeAt(0);
if (equals(range, existingRange)) {
return;
}
selection.removeAllRanges();
}
selection.addRange(range);
});
}
WindowEventListener({
onresize() {
updateControlBounds()
},
});
DocumentEventListener({
onSelectionchange(e) {
const selection = document.getSelection();
if (selection === null) {
return;
}
if (selection.rangeCount === 0) {
return;
}
if (document.activeElement !== ref()) {
return;
}
updateSelection(selection.getRangeAt(0)!);
},
});
createEventListenerMap(() => ref()!, {
keydown(e: KeyboardEvent) {
// keyCode === 229 is a special code that indicates an IME event.
// https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event#keydown_events_with_ime
if (e.keyCode === 229) {
console.log(e);
return;
}
const start = Math.min(context.selectionStart, context.selectionEnd);
let end = Math.max(context.selectionStart, context.selectionEnd);
if (e.key === 'Tab') {
e.preventDefault();
context.updateText(start, end, '&nbsp;&nbsp;&nbsp;&nbsp;');
} else if (e.key === 'Enter') {
context.updateText(start, end, '</p><p>&nbsp;');
}
},
});
onMount(() => {
updateControlBounds();
// updateSelection(indexMap.fromHtmlIndices(40, 60))
// updateSelection(indexMap.fromHtmlIndices(599, 603))
});
createEffect((last?: Element) => {
if (last !== undefined) {
last.editContext = undefined;
}
const el = ref();
if (el === undefined) {
return;
}
el.editContext = context;
return el;
});
return {
select(range: Range) {
updateSelection(range);
},
selection: createMemo<Range | undefined>(() => {
return store.selection;
}),
};
}
const observe = (node: Accessor<Node | undefined>): Accessor<readonly [Node | undefined, MutationRecord[]]> => {
const [mutations, setMutations] = createSignal<MutationRecord[]>([]);
const observer = new MutationObserver(records => {
setMutations(records);
});
createEffect(() => {
const n = node();
observer.disconnect();
if (n) {
observer.observe(n, { characterData: true, subtree: true, childList: true });
}
});
onCleanup(() => {
observer.disconnect();
});
return createMemo(() => [node(), mutations()] as const);
};
const parseProcessor = unified().use(rehypeParse)
const parse = (text: string) => parseProcessor.parse(text);
const equals = (a: Range, b: Range): boolean => {
const keys: (keyof Range)[] = ['startOffset', 'endOffset', 'commonAncestorContainer', 'startContainer', 'endContainer'];
return keys.every(key => a[key] === b[key]);
}
declare global {
interface Element {
editContext?: EditContext;
}
interface TextFormat {
readonly rangeStart: number;
readonly rangeEnd: number;
readonly underlineStyle: 'none' | 'solid' | 'double' | 'dotted' | 'sadhed' | 'wavy';
readonly underlineThickness: 'none' | 'thin' | 'thick';
}
interface CharacterBoundsUpdateEvent extends Event {
readonly rangeStart: number;
readonly rangeEnd: number;
}
interface TextFormatUpdateEvent extends Event {
getTextFormats(): TextFormat[];
}
interface TextUpdateEvent extends Event {
readonly updateRangeStart: number;
readonly updateRangeEnd: number;
readonly text: string;
readonly selectionStart: number;
readonly selectionEnd: number;
}
interface EditContextEventMap {
characterboundsupdate: CharacterBoundsUpdateEvent;
compositionstart: Event;
compositionend: Event;
textformatupdate: TextFormatUpdateEvent;
textupdate: TextUpdateEvent;
}
interface EditContext extends EventTarget {
readonly text: string;
readonly selectionStart: number;
readonly selectionEnd: number;
readonly characterBoundsRangeStart: number;
oncharacterboundsupdate?: (event: CharacterBoundsUpdateEvent) => any;
oncompositionstart?: (event: Event) => any;
oncompositionend?: (event: Event) => any;
ontextformatupdate?: (event: TextFormatUpdateEvent) => any;
ontextupdate?: (event: TextUpdateEvent) => any;
attachedElements(): [HTMLElement];
characterBounds(): DOMRect[];
updateText(rangeStart: number, rangeEnd: number, text: string): void;
updateSelection(start: number, end: number): void;
updateControlBounds(controlBounds: DOMRect): void;
updateSelectionBounds(selectionBounds: DOMRect): void;
updateCharacterBounds(rangeStart: number, characterBounds: DOMRect[]): void;
addEventListener<K extends keyof EditContextEventMap>(type: K, listener: (this: EditContext, ev: EditContextEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
}
interface EditContextConstructor {
new(): EditContext;
new(options: Partial<Pick<EditContext, 'text' | 'selectionStart' | 'selectionEnd'>>): EditContext;
readonly prototype: EditContext;
}
var EditContext: EditContextConstructor;
}

View file

@ -1,104 +0,0 @@
import { createContextProvider } from "@solid-primitives/context";
import { Accessor, createEffect, createMemo, createSignal, on, ParentProps, Setter, untrack } from "solid-js";
import { createEditor, SelectFunction } from "./context";
import { createSource, Source } from "../source";
import { getTextNodes } from "@solid-primitives/selection";
interface EditorContextType {
readonly text: Accessor<string>;
readonly selection: Accessor<Range | undefined>;
readonly source: Source;
select: SelectFunction;
}
interface EditorContextProps extends Record<string, unknown> {
ref: Accessor<Element | undefined>;
value: string;
oninput?: (value: string) => void;
}
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((props) => {
const source = createSource(() => props.value);
const { select, selection } = createEditor(props.ref, () => source.out, next => source.out = next);
createEffect(() => {
props.oninput?.(source.in);
});
createEffect(on(() => [props.ref()!, source.spellingErrors] as const, ([ref, errors]) => {
createHighlights(ref, 'spelling-error', errors);
}));
createEffect(on(() => [props.ref()!, source.grammarErrors] as const, ([ref, errors]) => {
createHighlights(ref, 'grammar-error', errors);
}));
createEffect(on(() => [props.ref()!, source.queryResults] as const, ([ref, results]) => {
createHighlights(ref, 'search-results', results);
}));
return {
text: () => source.out,
select,
source,
selection,
};
}, {
text: () => '',
selection: () => undefined,
source: {} as Source,
select() { },
});
export { useEditor };
export function Editor(props: ParentProps<{ value: string, oninput?: (value: string) => void }>) {
const [ref, setRef] = createSignal<Element>();
return <EditorProvider ref={ref} value={props.value} oninput={props.oninput}>
{props.children}
<Content ref={setRef} />
</EditorProvider>;
}
function Content(props: { ref: Setter<Element | undefined> }) {
const { text } = useEditor();
createEffect(on(text, () => console.error('rerendering')));
return <div ref={props.ref} innerHTML={text()} />;
}
const createHighlights = (node: Node, type: string, indices: [number, number][]) => {
queueMicrotask(() => {
const nodes = getTextNodes(node);
CSS.highlights.set(type, new Highlight(...indices.map(([start, end]) => indicesToRange(start, end, nodes))));
});
};
const indicesToRange = (start: number, end: number, textNodes: Node[]) => {
const [startNode, startPos] = getRangeArgs(start, textNodes);
const [endNode, endPos] = start === end ? [startNode, startPos] : getRangeArgs(end, textNodes);
const range = new Range();
if (startNode && endNode && startPos !== -1 && endPos !== -1) {
range.setStart(startNode, startPos);
range.setEnd(endNode, endPos);
}
return range;
}
const getRangeArgs = (offset: number, texts: Node[]): [node: Node | null, offset: number] =>
texts.reduce(
([node, pos], text) =>
node
? [node, pos]
: pos <= (text as Text).data.length
? [text, pos]
: [null, pos - (text as Text).data.length],
[null, offset] as [node: Node | null, pos: number],
);

View file

@ -1,54 +0,0 @@
import { describe, expect } from "vitest";
import { it } from "~/test-helpers";
import { hash } from "./hash";
const DEFAULT_DATA = {
prop_object: {
is: 'some prop',
},
prop_boolean: false,
prop_bigint: 1_000_000_000_000n,
prop_null: null,
prop_undefined: undefined,
prop_function: () => { },
prop_symbol: Symbol('symbol'),
uint8array: new Uint8Array([0xff, 0x00, 0xff, 0x00]),
uint32array: new Uint32Array([0xff00ff00]),
};
describe('hash', () => {
it('should hash a value with sha-1 algorithm', () => {
// Arrange
const expected = '6fe383b712ec74177f7714a3f5db5416accef8b';
// Act
const actual = hash(DEFAULT_DATA);
// Assert
expect(actual).toEqual(expected);
});
it('should be stable over multiple runs', () => {
// Arrange
// Act
const run1 = hash(DEFAULT_DATA);
const run2 = hash(DEFAULT_DATA);
// Assert
expect(run1).toEqual(run2);
});
// I can't seem to actually create a dataset that is large enough in order to test this.
// So, for now, I will consider this unreachable code.
it('should error if the input is too large', () => {
// Arrange
// Act
// Assert
expect(true).toEqual(true);
});
});

View file

@ -1,168 +0,0 @@
import { installIntoGlobal } from "iterator-helpers-polyfill";
installIntoGlobal();
const CHUNK_SIZE = 16;
const UINT32_BYTE_SIZE = 4;
const HASH_NUMBER_OF_UINT32 = 5;
const HASH_SIZE = HASH_NUMBER_OF_UINT32 * UINT32_BYTE_SIZE;
const initalizationVector /* 20 bytes */ = Object.freeze([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0] as const);
const hashKey /* 16 bytes */ = Object.freeze([0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6] as const);
type Word = number & {}; // union with empty object so typescript show this as 'Word' and not as 'number'
type HashBytes = Uint32Array & { length: typeof HASH_NUMBER_OF_UINT32 };
export const hash = (data: any) => {
const buffer = typeof data === 'object' && data instanceof Uint32Array ? data : new Uint32Array(toBinary(data));
if (!Number.isSafeInteger(buffer.length)) {
throw new Error('Cannot hash more than 2^53 - 1 bits');
}
// prepare blocks
const output = new Uint32Array(initalizationVector) as HashBytes;
const blocks = range(0, buffer.length, CHUNK_SIZE).map(i => {
const view = buffer.subarray(i, i + 16);
const words = Array<Word>(80);
words[0] = view[0];
words[1] = view[1];
words[2] = view[2];
words[3] = view[3];
words[4] = view[4];
return words;
});
// apply blocks
for (const words of blocks) {
let [a, b, c, d, e] = output;
for (let i = 0; i < 80; i++) {
if (i >= 16) {
words[i] = circularShiftLeft(1, words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16]);
}
const tmp = (
circularShiftLeft(a, HASH_NUMBER_OF_UINT32) +
logicalHashFunctions(i, b, c, d) +
e +
words[i] +
hashKey[Math.floor(i / HASH_SIZE)]
);
e = d;
d = c;
c = circularShiftLeft(b, 30);
b = a;
a = tmp;
}
output[0] = (output[0] + a) | 0;
output[1] = (output[1] + b) | 0;
output[2] = (output[2] + c) | 0;
output[3] = (output[3] + d) | 0;
output[4] = (output[4] + e) | 0;
}
return output.values().map(word => (word >>> 0).toString(16)).join('');
};
const circularShiftLeft = (subject: number, offset: number): number => {
return ((subject << offset) | (subject >>> 32 - offset)) & (0xFFFFFFFF);
};
const logicalHashFunctions = (index: number, b: Word, c: Word, d: Word): Word => {
if (index < HASH_SIZE) {
return (b & c) | (~b & d);
}
else if (index < (2 * HASH_SIZE)) {
return b ^ c ^ d;
}
else if (index < (3 * HASH_SIZE)) {
return (b & c) | (b & d) | (c & d);
}
else if (index < (4 * HASH_SIZE)) {
return b ^ c ^ d;
}
throw new Error('Unreachable code');
};
const range = function* (start: number, end: number, step: number): Iterator<number> {
for (let i = start; i <= end; i += step) {
yield i;
}
};
const toBinary = function*<T>(data: T): Generator<number, void, unknown> {
switch (typeof data) {
case 'function':
case 'symbol':
case 'undefined':
break;
case 'string':
yield* compact(new TextEncoder().encode(data));
break;
case 'number':
yield data;
break;
case 'boolean':
yield Number(data);
break;
case 'bigint':
let value: bigint = data;
// limit the iteration to 10 cycles.
// This covers 10*32 bits, which in al honesty should be enough no?
const ITERATION_LIMIT = 10;
for (let i = 0; i < ITERATION_LIMIT && value > 0; i++) {
yield Number((value & 0xffffffffn));
value >>= 32n;
if (i === 10) {
throw new Error('Iteration limit in bigint serialization reached');
}
}
break;
case 'object':
if (data === null) {
break;
}
if (data instanceof Uint8Array) {
yield* compact(data);
}
if (data instanceof Uint32Array) {
yield* data;
}
for (const item of Object.values(data)) {
yield* toBinary(item);
}
break;
}
};
const compact = function* (source: Iterable<number>): Generator<number, void, unknown> {
let i = 0;
let buffer = 0;
for (const value of source) {
buffer |= (value & 0xff) << (8 * i);
if (i === 3) {
yield buffer;
buffer = 0;
}
i = (i + 1) % 4;
}
};

View file

@ -1,3 +0,0 @@
export { createEditor as createEditContext } from './context';
export { Editor, useEditor } from './editor';
export { splitBy, createElement, mergeNodes } from './ast';

View file

@ -1,47 +0,0 @@
import type { Root, Text } from 'hast';
import { getTextNodes } from '@solid-primitives/selection';
import { Accessor, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
import { visit } from 'unist-util-visit';
export type IndexNode = { node: Text, dom: Node, text: { start: number, end: number }, html: { start: number, end: number } };
export type IndexMap = IndexNode[];
export type IndexRange = [IndexNode, IndexNode] | [undefined, undefined];
export function createMap(root: Accessor<Element | undefined>, ast: Accessor<Root>) {
const [mapping, setMapping] = createSignal(new WeakMap());
createEffect(() => {
const node = root();
const tree = ast();
if (node === undefined) {
return;
}
// Delay the recalculation a bit to give other code a chance to update the DOM.
// This -hopefully- prevents the map from getting out of sync
queueMicrotask(() => {
setMapping(createMapping(node, tree));
});
});
return {
query: (range: Range) => {
return [
mapping().get(range.startContainer),
mapping().get(range.endContainer),
];
},
};
}
const createMapping = (root: Node, ast: Root): WeakMap<Node, { start: number, end: number }> => {
const nodes = getTextNodes(root);
const map = new WeakMap();
visit(ast, (n): n is Text => n.type === 'text', (node) => {
map.set(nodes.shift()!, { start: node.position!.start.offset, end: node.position!.end.offset, text: node.value })
});
return map;
};

View file

@ -1,9 +1,10 @@
import { Accessor, Component, createEffect, createMemo, createSignal, For, JSX, Show, untrack } from "solid-js";
import { Mutation } from "~/utilities";
import { decode, Mutation } from "~/utilities";
import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid";
import { createDataSet, DataSetNode, DataSetRowNode } from "~/features/dataset";
import { SelectionItem } from "../selectable";
import { useI18n } from "../i18n";
import { debounce } from "@solid-primitives/scheduled";
import css from "./grid.module.css"
import { Textarea } from "~/components/textarea";

View file

@ -2,14 +2,17 @@ import { Accessor, createEffect, from, createSignal } from "solid-js";
import { json } from "./parser";
import { filter } from "~/utilities";
import { isServer } from "solid-js/web";
import { installIntoGlobal } from 'iterator-helpers-polyfill';
import { debounce } from "@solid-primitives/scheduled";
installIntoGlobal();
interface Files extends Record<string, { handle: FileSystemFileHandle, file: File }> { }
interface Contents extends Map<string, Map<string, string>> { }
export const read = (file: File): Promise<Map<string, string> | undefined> => {
switch (file.type) {
case 'application/json': return json.load(file.stream());
case 'application/json': return json.load(file.text());
default: return Promise.resolve(undefined);
}

View file

@ -1,200 +1,20 @@
import { decode } from "~/utilities";
export async function load(stream: ReadableStream<Uint8Array>): Promise<Map<string, string>> {
return new Map(await Array.fromAsync(parse(stream), ({ key, value }) => [key, value]));
}
interface Entry {
key: string;
value: string;
}
interface State {
(token: Token): State;
entry?: Entry
}
const states = {
none(): State {
return (token: Token) => {
if (token.kind === 'braceOpen') {
return states.object();
}
return states.none;
};
},
object({ path = [], expect = 'key' }: Partial<{ path: string[], expect: 'key' | 'colon' | 'value' }> = {}): State {
return (token: Token) => {
switch (expect) {
case 'key': {
if (token.kind === 'braceClose') {
return states.object({
path: path.slice(0, -1),
expect: 'key',
});
}
else if (token.kind === 'string') {
return states.object({
path: [...path, token.value],
expect: 'colon'
});
}
return states.error(`Expected a key, got ${token.kind} instead`);
}
case 'colon': {
if (token.kind !== 'colon') {
return states.error(`Expected a ':', got ${token.kind} instead`);
}
return states.object({
path,
expect: 'value'
});
}
case 'value': {
if (token.kind === 'braceOpen') {
return states.object({
path,
expect: 'key',
});
}
else if (token.kind === 'string') {
const next = states.object({
path: path.slice(0, -1),
expect: 'key',
});
next.entry = { key: path.join('.'), value: decode(token.value) };
return next
}
return states.error(`Invalid value type found '${token.kind}'`);
}
}
return states.none();
}
},
error(message: string): State {
throw new Error(message);
return states.none();
},
} as const;
async function* parse(stream: ReadableStream<Uint8Array>): AsyncGenerator<any, void, unknown> {
let state = states.none();
for await (const token of tokenize(read(toGenerator(stream)))) {
try {
state = state(token);
}
catch (e) {
console.error(e);
break;
}
if (state.entry) {
yield state.entry;
}
}
}
async function* take<T>(iterable: AsyncIterable<T>, numberToTake: number): AsyncGenerator<T, void, unknown> {
let i = 0;
for await (const entry of iterable) {
yield entry;
i++;
if (i === numberToTake) {
break;
}
}
}
type Token = { start: number, length: number } & (
| { kind: 'braceOpen' }
| { kind: 'braceClose' }
| { kind: 'colon' }
| { kind: 'string', value: string }
);
async function* tokenize(characters: AsyncIterable<number>): AsyncGenerator<Token, void, unknown> {
let buffer: string = '';
let clearBuffer = false;
let start = 0;
let i = 0;
for await (const character of characters) {
if (buffer.length === 0) {
start = i;
}
buffer += String.fromCharCode(character);
const length = buffer.length;
if (buffer === '{') {
yield { kind: 'braceOpen', start, length };
clearBuffer = true;
}
else if (buffer === '}') {
yield { kind: 'braceClose', start, length };
clearBuffer = true;
}
else if (buffer === ':') {
yield { kind: 'colon', start, length };
clearBuffer = true;
}
else if (buffer.length > 1 && buffer.startsWith('"') && buffer.endsWith('"') && buffer.at(-2) !== '\\') {
yield { kind: 'string', start, length, value: buffer.slice(1, buffer.length - 1) };
clearBuffer = true;
}
else if (buffer === ',') {
clearBuffer = true;
}
else if (buffer.trim() === '') {
clearBuffer = true;
}
if (clearBuffer) {
buffer = '';
clearBuffer = false;
}
i++;
}
}
async function* read(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<number, void, unknown> {
for await (const chunk of chunks) {
for (const character of chunk) {
yield character;
}
}
}
async function* toGenerator<T>(stream: ReadableStream<T>): AsyncGenerator<T, void, unknown> {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
yield value;
}
}
finally {
reader.releaseLock();
}
export async function load(text: Promise<string>): Promise<Map<string, string>> {
const source = JSON.parse(await text);
const result = new Map();
const candidates = Object.entries(source);
while (candidates.length !== 0) {
const [ key, value ] = candidates.shift()!;
if (typeof value !== 'object' || value === null || value === undefined) {
result.set(key, decode(value as string));
}
else {
candidates.unshift(...Object.entries(value).map<[string, any]>(([ k, v ]) => [`${key}.${k}`, v]));
}
}
return result;
}

View file

@ -1,4 +1,4 @@
import { Accessor, Component, createContext, createMemo, createResource, createSignal, For, JSX, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js";
import { Accessor, children, Component, createContext, createEffect, createMemo, createResource, createSignal, For, InitializedResource, JSX, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js";
import { AiFillFile, AiFillFolder, AiFillFolderOpen } from "solid-icons/ai";
import { SelectionProvider, selectable } from "~/features/selectable";
import { debounce } from "@solid-primitives/scheduled";

View file

@ -10,7 +10,7 @@ describe('Source', () => {
// Arrange
// Act
const actual = createSource(() => '');
const actual = createSource('');
// Assert
expect(actual.out).toBe('');
@ -22,7 +22,7 @@ describe('Source', () => {
const expected = '<p><strong>text</strong></p>';
// Act
const actual = createSource(() => given);
const actual = createSource(given);
// Assert
expect(actual.out).toBe(expected);
@ -31,7 +31,7 @@ describe('Source', () => {
it('should contain query results', () => {
// Arrange
const expected: [number, number][] = [[8, 9], [12, 13], [15, 16]];
const source = createSource(() => 'this is a seachable string');
const source = createSource('this is a seachable string');
// Act
source.query = 'a';

View file

@ -1,23 +1,21 @@
import { Accessor, createEffect, createMemo } from "solid-js";
import { createEffect, onMount } from "solid-js";
import { createStore } from "solid-js/store";
import { unified } from 'unified'
import { Text, Root } from 'hast'
import { visit } from "unist-util-visit";
import { decode } from "~/utilities";
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import remarkStringify from 'remark-stringify'
import rehypeParse from 'rehype-parse'
import rehypeDomParse from 'rehype-dom-parse'
import rehypeRemark from 'rehype-remark'
import rehypeStringify from 'rehype-stringify'
import type { Text, Root } from 'hast'
import { isServer } from "solid-js/web";
interface SourceStore {
in: string;
out: string;
plain: string;
query: RegExp;
query: string;
metadata: {
spellingErrors: [number, number][];
grammarErrors: [number, number][];
@ -28,7 +26,7 @@ interface SourceStore {
export interface Source {
in: string;
out: string;
query: RegExp;
query: string;
readonly spellingErrors: [number, number][];
readonly grammarErrors: [number, number][];
readonly queryResults: [number, number][];
@ -36,12 +34,29 @@ export interface Source {
// TODO :: make this configurable, right now we can only do markdown <--> html.
const inToOutProcessor = unified().use(remarkParse).use(remarkRehype).use(rehypeStringify);
const outToInProcessor = unified().use(isServer ? rehypeParse : rehypeDomParse).use(rehypeRemark).use(remarkStringify, { bullet: '-' });
const outToInProcessor = unified().use(rehypeParse).use(rehypeRemark).use(remarkStringify, { bullet: '-' });
export function createSource(value: Accessor<string>): Source {
const [store, setStore] = createStore<SourceStore>({ in: '', out: '', plain: '', query: new RegExp('', 'gi'), metadata: { spellingErrors: [], grammarErrors: [], queryResults: [] } });
export function createSource(initalValue: string): Source {
const ast = inToOutProcessor.runSync(inToOutProcessor.parse(initalValue));
const out = String(inToOutProcessor.stringify(ast));
const plain = String(unified().use(plainTextStringify).stringify(ast));
const src: Source = {
const [store, setStore] = createStore<SourceStore>({ in: initalValue, out, plain, query: '', metadata: { spellingErrors: [], grammarErrors: [], queryResults: [] } });
createEffect(() => {
const value = store.plain;
setStore('metadata', {
spellingErrors: spellChecker(value, ''),
grammarErrors: grammarChecker(value, ''),
});
});
createEffect(() => {
setStore('metadata', 'queryResults', findMatches(store.plain, store.query).toArray());
});
return {
get in() {
return store.in;
},
@ -87,26 +102,6 @@ export function createSource(value: Accessor<string>): Source {
return store.metadata.queryResults;
},
};
createEffect(() => {
src.in = value();
});
src.in = value();
createEffect(() => {
const value = store.plain;
setStore('metadata', {
spellingErrors: spellChecker(value, ''),
grammarErrors: grammarChecker(value, ''),
});
});
createEffect(() => {
setStore('metadata', 'queryResults', findMatches(store.plain, store.query));
});
return src;
}
function plainTextStringify() {
@ -121,10 +116,26 @@ function plainTextStringify() {
};
}
function findMatches(text: string, query: RegExp): [number, number][] {
return text.matchAll(query).map<[number, number]>(({ 0: match, index }) => {
return [index, index + match.length];
}).toArray();
function* findMatches(text: string, query: string): Generator<[number, number], void, unknown> {
if (query.length < 1) {
return;
}
let startIndex = 0;
while (startIndex < text.length) {
const index = text.indexOf(query, startIndex);
if (index === -1) {
break;
}
const end = index + query.length;
yield [index, end];
startIndex = end;
}
}
const spellChecker = checker(/\w+/gi);

View file

@ -35,7 +35,7 @@
"newKey": {
"title": "Which key do you want to create?",
"placeholder": "Name of the new key",
"description": "Hint: use `.` to denote nested keys,\n\ni.e. `this.is.some.key` would be a key that is four levels deep."
"description": "Hint: use `.` to denote nested keys,\ni.e. `this.is.some.key` would be a key that is four levels deep."
},
"newLanguage": {
"title": "Which language do you want to add?",

View file

@ -35,7 +35,7 @@
"newKey": {
"title": "Welke sleutel wil je toevoegen?",
"placeholder": "Naam van de nieuwe sleutel",
"description": "Hint: gebruik een `.` voor geneste sleutels,\n\nbijv. `this.is.some.key` is een sleutel die vier lagen diep is."
"description": "Hint: gebruik een `.` voor geneste sleutels,\nbijv. `this.is.some.key` is een sleutel die vier lagen diep is."
},
"newLanguage": {
"title": "Welke taal wil je toevoegen?",

View file

@ -17,8 +17,7 @@ export default function Experimental(props: ParentProps) {
<Menu.Item command={goTo.withLabel('table').with('table')} />
<Menu.Item command={goTo.withLabel('grid').with('grid')} />
<Menu.Item command={goTo.withLabel('context-menu').with('context-menu')} />
<Menu.Item command={goTo.withLabel('textarea').with('textarea')} />
<Menu.Item command={goTo.withLabel('editor').with('editor')} />
<Menu.Item command={goTo.withLabel('formatter').with('formatter')} />
<Menu.Item command={goTo.withLabel('file-system-observer').with('file-system-observer')} />
</Menu.Root>

View file

@ -1,73 +0,0 @@
.root {
position: relative;
margin: 1em;
padding: .5em;
gap: 1em;
display: grid;
grid: 100% / repeat(2, minmax(0, 1fr));
inline-size: calc(100% - 2em);
block-size: calc(100% - 2em);
place-content: start;
background-color: var(--surface-500);
border-radius: var(--radii-xl);
& > :is(textarea, .textarea) {
overflow: auto;
padding: .5em;
background-color: transparent;
}
& ::highlight(debug) {
text-decoration: double underline;
text-decoration-color: cornflowerblue;
}
& ::highlight(search-results) {
background-color: var(--secondary-900);
}
& ::highlight(spelling-error) {
text-decoration-line: spelling-error;
}
& ::highlight(grammar-error) {
text-decoration-line: grammar-error;
}
.editor {
display: block grid;
grid: auto 1fr / 100%;
.toolbar {
display: block grid;
grid-auto-flow: column;
place-content: start;
}
.search {
position: absolute;
inset-inline-end: 0;
inset-block-start: 0;
grid-template-columns: 1fr 1fr;
padding: .5em;
gap: .5em;
background-color: var(--surface-700);
border-radius: var(--radii-m);
box-shadow: var(--shadow-2);
&:popover-open {
display: block grid;
}
& > label {
display: contents;
}
}
}
}

View file

@ -1,129 +0,0 @@
import { createEffect, createMemo, createSignal, onMount, untrack } from "solid-js";
import { debounce } from "@solid-primitives/scheduled";
import { Editor, useEditor } from "~/features/editor";
import css from './editor.module.css';
import { assert } from "~/utilities";
const tempVal = `
# Header
this is **a string** that contains bolded text
this is *a string* that contains italicized text
> Dorothy followed her through many of the beautiful rooms in her castle.
> #### The quarterly results look great!
>
> - Revenue was off the chart.
> - Profits were higher than ever.
>
> > The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
>
> *Everything* is going according to **plan**.
- First item
- Second item
- Third item
- Fourth item
`;
export default function Formatter(props: {}) {
const [value, setValue] = createSignal(tempVal);
const onInput = (e: InputEvent) => {
setValue((e.target! as HTMLTextAreaElement).value);
};
return <div class={css.root}>
<textarea oninput={onInput} title="markdown">{value()}</textarea>
<div class={css.editor}>
<Editor value={value()} oninput={setValue}>
<Toolbar />
<SearchAndReplace />
</Editor>
</div>
</div>;
}
function Toolbar() {
const { selection } = useEditor();
const bold = () => {
const range = untrack(selection)!;
console.log(range);
if (range.startContainer.nodeType !== Node.TEXT_NODE) {
return;
}
if (range.endContainer.nodeType !== Node.TEXT_NODE) {
return;
}
// Trim whitespace
{
const text = range.toString();
range.setStart(range.startContainer, range.startOffset + (text.match(/^\s+/)?.[0].length ?? 0));
range.setEnd(range.endContainer, range.endOffset - (text.match(/\s+$/)?.[0].length ?? 0));
}
const fragment = range.extractContents();
if (range.startContainer === range.commonAncestorContainer && range.endContainer === range.commonAncestorContainer && range.commonAncestorContainer.parentElement?.tagName === 'STRONG') {
range.selectNode(range.commonAncestorContainer.parentElement);
range.insertNode(fragment);
}
else {
const strong = document.createElement('strong');
strong.append(fragment);
range.insertNode(strong);
range.selectNode(strong);
}
};
onMount(() => {
queueMicrotask(() => {
// bold();
});
});
return <div class={css.toolbar}>
<button onclick={bold}>bold</button>
</div>;
}
function SearchAndReplace() {
const { source } = useEditor();
const [replacement, setReplacement] = createSignal('');
const [term, setTerm] = createSignal('');
const [caseInsensitive, setCaseInsensitive] = createSignal(true);
const query = createMemo(() => new RegExp(term(), caseInsensitive() ? 'gi' : 'g'));
createEffect(() => {
source.query = query();
});
const replace = (e: SubmitEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
form.reset();
console.log(source.queryResults);
// mutate(text => text.replaceAll(query(), replacement()));
};
return <form on:submit={replace} class={css.search} popover="manual">
<label><span>Case insensitive</span><input type="checkbox" checked={caseInsensitive()} oninput={e => setCaseInsensitive(e.target.checked)} /></label>
<label><span>Search for</span><input type="search" title="editor-search" oninput={e => setTerm(e.target.value)} /></label>
<label><span>Replace with</span><input type="search" title="editor-replace" oninput={e => setReplacement(e.target.value)} /></label>
<button>replace</button>
</form>;
};

View file

@ -0,0 +1,22 @@
.root {
position: relative;
margin: 1em;
padding: .5em;
gap: 1em;
display: grid;
grid: 100% / repeat(2, minmax(0, 1fr));
inline-size: calc(100% - 2em);
block-size: calc(100% - 2em);
place-content: start;
background-color: var(--surface-500);
border-radius: var(--radii-xl);
& > :is(textarea, .textarea) {
overflow: auto;
padding: .5em;
background-color: transparent;
}
}

View file

@ -1,7 +1,7 @@
import { createSignal, untrack } from "solid-js";
import { createSignal } from "solid-js";
import { debounce } from "@solid-primitives/scheduled";
import { Textarea } from "~/components/textarea";
import css from './textarea.module.css';
import css from './formatter.module.css';
const tempVal = `
# Header
@ -12,13 +12,15 @@ this is *a string* that contains italicized text
> Dorothy followed her through many of the beautiful rooms in her castle.
> Dorothy followed her through many of the beautiful rooms in her castle.
>
> > The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
> #### The quarterly results look great!
>
> - Revenue was off the chart.
> - Profits were higher than ever.
>
> > The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
>
> *Everything* is going according to **plan**.
- First item
@ -35,7 +37,7 @@ export default function Formatter(props: {}) {
}, 300);
return <div class={css.root}>
<textarea oninput={onInput} title="markdown">{value()}</textarea>
<Textarea class={css.textarea} title="html" value={value()} oninput={setValue} lang="en-GB" />
<textarea oninput={onInput}>{value()}</textarea>
<Textarea class={css.textarea} value={value()} oninput={setValue} lang="en-GB" />
</div>;
}

View file

@ -1,7 +1,7 @@
import { Sidebar } from '~/components/sidebar';
import { CellEditor, Column, DataSetGroupNode, DataSetNode, DataSetRowNode, Grid, GridApi } from '~/components/grid';
import { people, Person } from './experimental.data';
import { Component, createEffect, createMemo, createSignal, For } from 'solid-js';
import { Component, createEffect, createMemo, createSignal, For, Match, Switch } from 'solid-js';
import { MutarionKind, Mutation } from '~/utilities';
import { Table } from '~/components/table';
import { createDataSet } from '~/features/dataset';
@ -71,10 +71,6 @@ export default function GridExperiment() {
sort: { by: 'name', reversed: false },
});
createEffect(() => {
console.log(rows);
});
return <div class={css.root}>
<Sidebar as="aside" label={'Grid options'} class={css.sidebar}>
<fieldset>
@ -111,13 +107,13 @@ type M = { kind: MutarionKind, key: string, original?: any, value?: any };
const Mutations: Component<{ mutations: Mutation[] }> = (props) => {
const columns: Column<M>[] = [{ id: 'key', label: 'Key' }, { id: 'original', label: 'Old' }, { id: 'value', label: 'New' }];
const rows = createDataSet<M>(() => props.mutations);
const rows = createMemo(() => createDataSet<M>(props.mutations));
createEffect(() => {
rows.group({ by: 'kind' });
rows().group({ by: 'kind' });
});
return <Table rows={rows} columns={columns}>{{
return <Table rows={rows()} columns={columns}>{{
original: ({ value }) => value ? <del><pre>{JSON.stringify(value, null, 2)}</pre></del> : null,
value: ({ value }) => value ? <ins><pre>{JSON.stringify(value, null, 2)}</pre></ins> : null,
}}</Table>

View file

@ -1,50 +0,0 @@
.root {
position: relative;
margin: 1em;
padding: .5em;
gap: 1em;
display: grid;
grid: 100% / repeat(2, minmax(0, 1fr));
inline-size: calc(100% - 2em);
block-size: calc(100% - 2em);
place-content: start;
background-color: var(--surface-500);
border-radius: var(--radii-xl);
& > :is(textarea, .textarea) {
overflow: auto;
padding: .5em;
background-color: transparent;
}
& ::highlight(search-results) {
background-color: var(--secondary-900);
}
& ::highlight(spelling-error) {
text-decoration-line: spelling-error;
}
& ::highlight(grammar-error) {
text-decoration-line: grammar-error;
}
.search {
position: absolute;
inset-inline-end: 0;
inset-block-start: 0;
display: block grid;
grid-auto-flow: row;
padding: .5em;
gap: .5em;
background-color: var(--surface-700);
border-radius: var(--radii-m);
box-shadow: var(--shadow-2);
}
}

View file

@ -1,5 +1,5 @@
import { describe, expect, vi } from 'vitest';
import { decode, deepCopy, deepDiff, filter, gen__split_by_filter, map, MutarionKind, splice, split_by_filter, splitAt } from './utilities';
import { decode, deepCopy, deepDiff, filter, gen__split_by_filter, map, MutarionKind, split_by_filter, splitAt } from './utilities';
import { it } from '~/test-helpers';
const { spyOn } = vi;
@ -11,44 +11,6 @@ const first = <T>(iterable: Iterable<T>): T | undefined => {
}
describe('utilities', () => {
describe('splice', () => {
it('can replace part of string based on indices', async () => {
// Arrange
const given = 'this is a string';
const expected = 'this was a string';
// Act
const actual = splice(given, 5, 7, 'was');
// Assert
expect(actual).toBe(expected);
});
it('can replace from the start', async () => {
// Arrange
const given = 'this is a string';
const expected = 'was a string';
// Act
const actual = splice(given, 0, 7, 'was');
// Assert
expect(actual).toBe(expected);
});
it('can replace till the end', async () => {
// Arrange
const given = 'this is a string';
const expected = 'this was';
// Act
const actual = splice(given, 5, -0, 'was');
// Assert
expect(actual).toBe(expected);
});
});
describe('splitAt', () => {
it('should split the given string at the given index', async () => {
// Arrange

View file

@ -1,12 +1,3 @@
export const assert = (assertion: boolean, message: string) => {
if (assertion !== true) {
throw new Error(message);
}
}
export const splice = (subject: string, start: number, end: number, replacement: string) => {
return `${subject.slice(0, start)}${replacement}${Object.is(end, -0) ? '' : subject.slice(end)}`;
};
export const splitAt = (subject: string, index: number): readonly [string, string] => {
if (index < 0) {
return [subject, ''];
@ -52,20 +43,6 @@ const decodeReplacer = (_: any, char: EncodedChar) => ({
}[char.charAt(0) as ('t' | 'b' | 'n' | 'r' | 'f' | '\'' | '"' | 'u')]);
export const decode = (subject: string): string => subject.replace(decodeRegex, decodeReplacer);
const LAZY_SYMBOL = Symbol('not loaded');
export const lazy = <T>(fn: () => T): (() => T) => {
let value: T | symbol = LAZY_SYMBOL;
return () => {
if (value === LAZY_SYMBOL) {
value = fn();
}
return value as T;
}
};
/** @deprecated just use structuredClone instead */
export const deepCopy = <T>(original: T): T => {
if (typeof original !== 'object' || original === null || original === undefined) {
return original;
@ -149,8 +126,7 @@ export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, pa
}
};
const nonIterableTypes = ['boolean', 'undefined', 'null', 'number'];
const isIterable = (subject: object): subject is Iterable<any> => nonIterableTypes.includes(typeof subject) === false;
const isIterable = (subject: object): subject is Iterable<any> => ['boolean', 'undefined', 'null', 'number'].includes(typeof subject) === false;
const entriesOf = (subject: object): Iterable<readonly [string | number, any]> => {
if (subject instanceof Array) {
return subject.entries();

View file

@ -14,9 +14,9 @@
"@solidjs/start/env",
"@testing-library/jest-dom",
"@types/wicg-file-system-access",
"@vitest/browser/providers/playwright",
"vinxi/types/client",
"vite-plugin-solid-svg/types-component-solid",
"vite-plugin-pwa/solid",
"bun-types"
],
"isolatedModules": true,

View file

@ -28,7 +28,7 @@ export default defineConfig({
provider: 'istanbul',
reportsDirectory: './.coverage',
all: false,
}
},
},
});
@ -39,13 +39,6 @@ function reportWith(...reporter: CoverageReporter[]): Plugin {
config(userConf, env) {
if (userConf.test) {
userConf.test.coverage = { ...userConf.test.coverage, reporter } as CoverageV8Options;
userConf.test.browser = {
provider: 'playwright',
enabled: true,
headless: true,
screenshotFailures: false,
instances: [{ browser: 'chromium' }]
};
}
},
}