mirror of
https://github.com/TextPlace/CoreTextPlace.git
synced 2025-12-16 20:32:34 +00:00
Compare commits
23 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8746ae1494 | |||
| d53e627440 | |||
| c703b1870a | |||
| d97546f47c | |||
| 9d114182cf | |||
| 0c901f0144 | |||
| cfe0fb1514 | |||
| 78898cc90b | |||
| b51545ab39 | |||
| 34e710f036 | |||
| 6ae18c15b7 | |||
| 5d862c0bf7 | |||
| bf976a8ceb | |||
| 4bcbd865d3 | |||
| 32534084df | |||
|
|
7923680e80 | ||
|
|
4dd8121ebb | ||
|
|
8940f26f17 | ||
|
|
97ad480ad8 | ||
|
|
645b720222 | ||
|
|
46a647441b | ||
|
|
587e0558e9 | ||
|
|
91db478fe9 |
34 changed files with 2863 additions and 652 deletions
41
.github/workflows/deno.yml
vendored
41
.github/workflows/deno.yml
vendored
|
|
@ -1,41 +0,0 @@
|
|||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# This workflow will install Deno then run `deno lint` and `deno test`.
|
||||
# For more information see: https://github.com/denoland/setup-deno
|
||||
|
||||
name: Deno
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Setup repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
# Uncomment this step to verify the use of 'deno fmt' on each commit.
|
||||
- name: Verify formatting
|
||||
run: deno fmt --check
|
||||
|
||||
- name: Run linter
|
||||
run: deno lint
|
||||
|
||||
- name: Run tests
|
||||
run: deno test -A
|
||||
38
.github/workflows/publish_jsr.yml
vendored
38
.github/workflows/publish_jsr.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
name: Publish on JSR
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Deno"]
|
||||
workflows: ["Test"]
|
||||
types: [completed]
|
||||
branches:
|
||||
- "main"
|
||||
|
|
@ -9,6 +9,7 @@ on:
|
|||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -17,5 +18,36 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Publish package
|
||||
run: npx jsr publish
|
||||
- name: Setup repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
# This step removes JSR dependencies from package.json before publishing,
|
||||
# Because apparently now while major npm compatible package managers
|
||||
# support JSR dependencies in package.json, JSR itself does not.
|
||||
- name: Remove JSR deps from package.json
|
||||
run: |
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
||||
for (const dep in pkg.dependencies ?? {}) {
|
||||
if (pkg.dependencies[dep].startsWith('jsr:')) {
|
||||
delete pkg.dependencies[dep];
|
||||
}
|
||||
}
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
|
||||
- name: Publish to JSR
|
||||
# Have to use --allow-dirty because we modified package.json in the previous step
|
||||
run: pnpm dlx jsr publish --allow-dirty
|
||||
|
|
|
|||
19
.github/workflows/publish_npm.yml
vendored
19
.github/workflows/publish_npm.yml
vendored
|
|
@ -9,18 +9,21 @@ jobs:
|
|||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
- name: Setup repo
|
||||
uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
deno-version: v2.x
|
||||
run_install: false
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
node-version: "24.x"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: deno run -A scripts/build_npm.ts
|
||||
- run: cd npm && npm ci
|
||||
- run: cd npm && npm publish --provenance --access public
|
||||
- run: pnpm install
|
||||
- run: pnpm build
|
||||
- run: pnpm test
|
||||
# --no-git-checks because of https://github.com/pnpm/pnpm/issues/5894.
|
||||
- run: pnpm publish --provenance --access public --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
|||
32
.github/workflows/test.yml
vendored
Normal file
32
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Setup repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
npm
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
|
|
|
|||
|
|
@ -3,25 +3,11 @@
|
|||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||
{
|
||||
"formatter": "prettier",
|
||||
"format_on_save": "on",
|
||||
"languages": {
|
||||
"TypeScript": {
|
||||
"language_servers": ["deno", "!typescript-language-server", "!eslint"],
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "deno",
|
||||
"arguments": ["fmt", "-"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["deno", "!typescript-language-server", "!eslint"],
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "deno",
|
||||
"arguments": ["fmt", "-"]
|
||||
}
|
||||
}
|
||||
"language_servers": ["vtsls", "!deno"]
|
||||
}
|
||||
},
|
||||
"format_on_save": "on"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
// Static tasks configuration.
|
||||
//
|
||||
// Example:
|
||||
[
|
||||
{
|
||||
"label": "Test",
|
||||
"command": "deno test"
|
||||
"command": "pnpm test"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,5 +1,5 @@
|
|||
The MIT License (MIT)
|
||||
Copyright © 2024 Shibo Lyu <hi@lao.sb>
|
||||
Copyright © 2025 Shibo Lyu <hi@lao.sb>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
|
|||
5
build.config.ts
Normal file
5
build.config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { defineBuildConfig } from "obuild/config";
|
||||
|
||||
export default defineBuildConfig({
|
||||
entries: ["src/mod.ts"],
|
||||
}) as ReturnType<typeof defineBuildConfig>;
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
{
|
||||
"name": "@textplace/core",
|
||||
"version": "0.3.0",
|
||||
"exports": "./mod.ts",
|
||||
"version": "0.6.1",
|
||||
"exports": "./src/mod.ts",
|
||||
"imports": {
|
||||
"@deno/dnt": "jsr:@deno/dnt@^0.41.3"
|
||||
"@std/cli": "jsr:@std/cli@1"
|
||||
},
|
||||
"publish": {
|
||||
"include": ["src/**/*.ts", "LICENSE", "README.md"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
deno.lock
generated
157
deno.lock
generated
|
|
@ -1,157 +0,0 @@
|
|||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@david/code-block-writer@^13.0.2": "13.0.3",
|
||||
"jsr:@deno/cache-dir@~0.10.3": "0.10.3",
|
||||
"jsr:@deno/dnt@~0.41.3": "0.41.3",
|
||||
"jsr:@std/assert@0.223": "0.223.0",
|
||||
"jsr:@std/assert@0.226": "0.226.0",
|
||||
"jsr:@std/bytes@0.223": "0.223.0",
|
||||
"jsr:@std/fmt@0.223": "0.223.0",
|
||||
"jsr:@std/fmt@1": "1.0.3",
|
||||
"jsr:@std/fs@0.223": "0.223.0",
|
||||
"jsr:@std/fs@1": "1.0.6",
|
||||
"jsr:@std/fs@~0.229.3": "0.229.3",
|
||||
"jsr:@std/io@0.223": "0.223.0",
|
||||
"jsr:@std/path@0.223": "0.223.0",
|
||||
"jsr:@std/path@1": "1.0.8",
|
||||
"jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1",
|
||||
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||
"jsr:@std/path@~0.225.2": "0.225.2",
|
||||
"jsr:@ts-morph/bootstrap@0.24": "0.24.0",
|
||||
"jsr:@ts-morph/common@0.24": "0.24.0"
|
||||
},
|
||||
"jsr": {
|
||||
"@david/code-block-writer@13.0.3": {
|
||||
"integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f"
|
||||
},
|
||||
"@deno/cache-dir@0.10.3": {
|
||||
"integrity": "eb022f84ecc49c91d9d98131c6e6b118ff63a29e343624d058646b9d50404776",
|
||||
"dependencies": [
|
||||
"jsr:@std/fmt@0.223",
|
||||
"jsr:@std/fs@0.223",
|
||||
"jsr:@std/io",
|
||||
"jsr:@std/path@0.223"
|
||||
]
|
||||
},
|
||||
"@deno/dnt@0.41.3": {
|
||||
"integrity": "b2ef2c8a5111eef86cb5bfcae103d6a2938e8e649e2461634a7befb7fc59d6d2",
|
||||
"dependencies": [
|
||||
"jsr:@david/code-block-writer",
|
||||
"jsr:@deno/cache-dir",
|
||||
"jsr:@std/fmt@1",
|
||||
"jsr:@std/fs@1",
|
||||
"jsr:@std/path@1",
|
||||
"jsr:@ts-morph/bootstrap"
|
||||
]
|
||||
},
|
||||
"@std/assert@0.223.0": {
|
||||
"integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24"
|
||||
},
|
||||
"@std/assert@0.226.0": {
|
||||
"integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3"
|
||||
},
|
||||
"@std/bytes@0.223.0": {
|
||||
"integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8"
|
||||
},
|
||||
"@std/fmt@0.223.0": {
|
||||
"integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208"
|
||||
},
|
||||
"@std/fmt@1.0.3": {
|
||||
"integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f"
|
||||
},
|
||||
"@std/fs@0.223.0": {
|
||||
"integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c"
|
||||
},
|
||||
"@std/fs@0.229.3": {
|
||||
"integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb",
|
||||
"dependencies": [
|
||||
"jsr:@std/path@1.0.0-rc.1"
|
||||
]
|
||||
},
|
||||
"@std/fs@1.0.6": {
|
||||
"integrity": "42b56e1e41b75583a21d5a37f6a6a27de9f510bcd36c0c85791d685ca0b85fa2",
|
||||
"dependencies": [
|
||||
"jsr:@std/path@^1.0.8"
|
||||
]
|
||||
},
|
||||
"@std/io@0.223.0": {
|
||||
"integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@0.223",
|
||||
"jsr:@std/bytes"
|
||||
]
|
||||
},
|
||||
"@std/path@0.223.0": {
|
||||
"integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@0.223"
|
||||
]
|
||||
},
|
||||
"@std/path@0.225.2": {
|
||||
"integrity": "0f2db41d36b50ef048dcb0399aac720a5348638dd3cb5bf80685bf2a745aa506",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@0.226"
|
||||
]
|
||||
},
|
||||
"@std/path@1.0.0-rc.1": {
|
||||
"integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6"
|
||||
},
|
||||
"@std/path@1.0.8": {
|
||||
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||
},
|
||||
"@ts-morph/bootstrap@0.24.0": {
|
||||
"integrity": "a826a2ef7fa8a7c3f1042df2c034d20744d94da2ee32bf29275bcd4dffd3c060",
|
||||
"dependencies": [
|
||||
"jsr:@ts-morph/common"
|
||||
]
|
||||
},
|
||||
"@ts-morph/common@0.24.0": {
|
||||
"integrity": "12b625b8e562446ba658cdbe9ad77774b4bd96b992ae8bd34c60dbf24d06c1f3",
|
||||
"dependencies": [
|
||||
"jsr:@std/fs@~0.229.3",
|
||||
"jsr:@std/path@~0.225.2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@deno/dnt@~0.41.3"
|
||||
]
|
||||
}
|
||||
}
|
||||
114
logic/board.ts
114
logic/board.ts
|
|
@ -1,114 +0,0 @@
|
|||
import type { SectionData, SectionPosition } from "../types/section.ts";
|
||||
import type {
|
||||
BoardConfig,
|
||||
BoardData,
|
||||
CharacterPosition,
|
||||
FullBoard,
|
||||
} from "../types/board.ts";
|
||||
import { applyChange, createSection } from "./section.ts";
|
||||
import type { BoardChange } from "../types/change.ts";
|
||||
|
||||
export function createBoard(config: BoardConfig): BoardData {
|
||||
return { config, sections: [] };
|
||||
}
|
||||
|
||||
export function locateSection(
|
||||
{ x, y }: CharacterPosition,
|
||||
config: BoardConfig,
|
||||
): SectionPosition {
|
||||
return {
|
||||
sx: Math.floor(x / config.sectionWidth),
|
||||
sy: Math.floor(y / config.sectionHeight),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section from board.
|
||||
*
|
||||
* If the section does not exist yet, it will be created and (optionally) added to `board`.
|
||||
*/
|
||||
export function getSectionOnBoard(
|
||||
{ sx, sy }: SectionPosition,
|
||||
board: BoardData,
|
||||
options: {
|
||||
/**
|
||||
* Whether the section data is only used for reading.
|
||||
*
|
||||
* If `true`, this function will return an empty section configured with default values, but will not add it to the board data to save storage.
|
||||
*/
|
||||
readOnly: boolean;
|
||||
} = { readOnly: false },
|
||||
): SectionData {
|
||||
let section: SectionData;
|
||||
if (!board.sections[sy] && !options.readOnly) board.sections[sy] = [];
|
||||
|
||||
if (!board.sections[sy]?.[sx]) {
|
||||
section = createSection({ sx, sy }, board.config);
|
||||
if (!options.readOnly) board.sections[sy][sx] = section;
|
||||
} else {
|
||||
section = board.sections[sy][sx];
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
export function applyChangeOnBoard(change: BoardChange, board: BoardData) {
|
||||
const sPos = locateSection(change, board.config);
|
||||
const section = getSectionOnBoard(sPos, board);
|
||||
applyChange(change, section);
|
||||
}
|
||||
|
||||
export function renderFullBoard(data: BoardData): FullBoard {
|
||||
const totalLineCount = data.config.sectionHeight * data.config.ySections;
|
||||
const lineLength = data.config.sectionWidth * data.config.xSections;
|
||||
|
||||
const chLines: string[][] = Array(totalLineCount);
|
||||
const colorLines: string[][] = Array(totalLineCount);
|
||||
const bgColorLines: string[][] = Array(totalLineCount);
|
||||
const widthLines: number[][] = Array(totalLineCount);
|
||||
|
||||
for (let y = 0; y < totalLineCount; y++) {
|
||||
const chLine: string[] = [];
|
||||
const colorLine: string[] = [];
|
||||
const bgColorLine: string[] = [];
|
||||
const widthLine: number[] = [];
|
||||
|
||||
let charsToSkip = 0;
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
if (charsToSkip > 0) {
|
||||
charsToSkip--;
|
||||
continue;
|
||||
}
|
||||
|
||||
const sPos = locateSection({ x, y }, data.config);
|
||||
const section = getSectionOnBoard(sPos, data, { readOnly: true });
|
||||
const xInSection = x % data.config.sectionWidth;
|
||||
const yInSection = y % data.config.sectionHeight;
|
||||
|
||||
const cCh = section.ch[yInSection][xInSection];
|
||||
const cCo = section.color[yInSection][xInSection];
|
||||
const cBg = section.bgColor[yInSection][xInSection];
|
||||
const cWd = section.width[yInSection][xInSection];
|
||||
|
||||
chLine.push(cCh);
|
||||
colorLine.push(cCo);
|
||||
bgColorLine.push(cBg);
|
||||
widthLine.push(cWd);
|
||||
charsToSkip += cWd - 1;
|
||||
}
|
||||
|
||||
chLines[y] = chLine;
|
||||
colorLines[y] = colorLine;
|
||||
bgColorLines[y] = bgColorLine;
|
||||
widthLines[y] = widthLine;
|
||||
}
|
||||
|
||||
return {
|
||||
w: lineLength,
|
||||
h: totalLineCount,
|
||||
ch: ([] as string[]).concat(...chLines).flat(),
|
||||
color: ([] as string[]).concat(...colorLines).flat(),
|
||||
bg_color: ([] as string[]).concat(...bgColorLines).flat(),
|
||||
width: ([] as number[]).concat(...widthLines).flat(),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
||||
const cjkRegex =
|
||||
/[\p{Unified_Ideograph}\u30A0-\u30FF\u3040-\u309F\u31F0-\u31FF]/u;
|
||||
const printableASCIIRegex = /^[\x20-\x7E]$/;
|
||||
|
||||
export function getCharacterWidth(ch: string): number {
|
||||
const segments = [...segmenter.segment(ch)];
|
||||
if (segments.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one grapheme cluster, got ${segments.length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const matchesASCII = ch.match(printableASCIIRegex);
|
||||
const matchesCJK = ch.match(cjkRegex);
|
||||
|
||||
if (!matchesASCII && !matchesCJK) throw new Error(`Invalid character: ${ch}`);
|
||||
|
||||
// TODO: Support Emojis.
|
||||
return matchesCJK ? 2 : 1;
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { getCharacterWidth } from "../mod.ts";
|
||||
import type { BoardConfig } from "../types/board.ts";
|
||||
import type { BoardChange } from "../types/change.ts";
|
||||
import type { SectionData, SectionPosition } from "../types/section.ts";
|
||||
|
||||
export function createSection(
|
||||
{ sx, sy }: SectionPosition,
|
||||
boardConfig: BoardConfig,
|
||||
): SectionData {
|
||||
if (boardConfig.sectionWidth % 2 !== 0) {
|
||||
throw new Error(
|
||||
"sectionWidth must be multiple of 2 (least common multiples of all character widths)",
|
||||
);
|
||||
}
|
||||
|
||||
const offsetX = sx * boardConfig.sectionWidth;
|
||||
const offsetY = sy * boardConfig.sectionHeight;
|
||||
|
||||
const ch: string[][] = Array(boardConfig.sectionHeight).fill(
|
||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultCh),
|
||||
);
|
||||
const color: string[][] = Array(boardConfig.sectionHeight).fill(
|
||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultColor),
|
||||
);
|
||||
const bgColor: string[][] = Array(boardConfig.sectionHeight).fill(
|
||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultBgColor),
|
||||
);
|
||||
const width: number[][] = Array(boardConfig.sectionHeight).fill(
|
||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultWidth),
|
||||
);
|
||||
|
||||
return { offsetX, offsetY, ch, color, bgColor, width };
|
||||
}
|
||||
|
||||
export function applyChange(change: BoardChange, section: SectionData) {
|
||||
const xInSection = change.x - section.offsetX;
|
||||
const yInSection = change.y - section.offsetY;
|
||||
|
||||
const validX = xInSection >= 0 && xInSection < section.ch[0].length;
|
||||
const validY = yInSection >= 0 && yInSection < section.ch.length;
|
||||
if (!validX || !validY) {
|
||||
throw new Error("Change does not belong to this section");
|
||||
}
|
||||
|
||||
if (change.ch) {
|
||||
const chWidth = getCharacterWidth(change.ch);
|
||||
const xCharacterOffset = xInSection % 2;
|
||||
const offsetAdjustedXInSection = xInSection - xCharacterOffset;
|
||||
section.ch[yInSection][offsetAdjustedXInSection] = change.ch;
|
||||
section.width[yInSection][offsetAdjustedXInSection] = chWidth;
|
||||
}
|
||||
if (change.color) {
|
||||
section.color[yInSection][xInSection] = change.color;
|
||||
}
|
||||
if (change.bg_color) {
|
||||
section.bgColor[yInSection][xInSection] = change.bg_color;
|
||||
}
|
||||
}
|
||||
7
mod.ts
7
mod.ts
|
|
@ -1,7 +0,0 @@
|
|||
export * from "./types/board.ts";
|
||||
export * from "./types/section.ts";
|
||||
export * from "./types/change.ts";
|
||||
|
||||
export * from "./logic/board.ts";
|
||||
export * from "./logic/section.ts";
|
||||
export * from "./logic/character.ts";
|
||||
38
package.json
Normal file
38
package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "@textplace/core",
|
||||
"version": "0.6.1",
|
||||
"description": "The core logic of TextPlace.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TextPlace/CoreTextPlace"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/TextPlace/CoreTextPlace/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"build": "obuild",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/mod.d.ts",
|
||||
"import": "./dist/mod.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@std/cli": "jsr:^1.0.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"obuild": "^0.4.3",
|
||||
"prettier": "^3.7.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.14"
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0"
|
||||
}
|
||||
1897
pnpm-lock.yaml
generated
Normal file
1897
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { build, emptyDir } from "@deno/dnt";
|
||||
|
||||
import denoJson from "../deno.json" with { type: "json" };
|
||||
|
||||
await emptyDir("./npm");
|
||||
|
||||
await build({
|
||||
entryPoints: ["./mod.ts"],
|
||||
outDir: "./npm",
|
||||
shims: {
|
||||
// see JS docs for overview and more options
|
||||
deno: true,
|
||||
},
|
||||
compilerOptions: {
|
||||
lib: ["ES2022"],
|
||||
},
|
||||
package: {
|
||||
// package.json properties
|
||||
name: "@textplace/core",
|
||||
version: denoJson.version,
|
||||
description: "The core logic of TextPlace.",
|
||||
license: "MIT",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: "https://github.com/TextPlace/CoreTextPlace",
|
||||
},
|
||||
bugs: {
|
||||
url: "https://github.com/TextPlace/CoreTextPlace/issues",
|
||||
},
|
||||
},
|
||||
postBuild() {
|
||||
// steps to run after building and before running the tests
|
||||
Deno.copyFileSync("LICENSE", "npm/LICENSE");
|
||||
Deno.copyFileSync("README.md", "npm/README.md");
|
||||
},
|
||||
});
|
||||
55
src/logic/board.ts
Normal file
55
src/logic/board.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type { SectionData, SectionPosition } from "../types/section";
|
||||
import type { BoardConfig, BoardData, CharacterPosition } from "../types/board";
|
||||
import { applyChange, createSection } from "./section";
|
||||
import type { BoardChange } from "../types/change";
|
||||
|
||||
export function createBoard(config: BoardConfig): BoardData {
|
||||
return { config, sections: [] };
|
||||
}
|
||||
|
||||
export function locateSection(
|
||||
{ x, y }: CharacterPosition,
|
||||
config: BoardConfig,
|
||||
): SectionPosition {
|
||||
return {
|
||||
sx: Math.floor(x / config.sectionWidth),
|
||||
sy: Math.floor(y / config.sectionHeight),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section from board.
|
||||
*
|
||||
* If the section does not exist yet, it will be created and (optionally) added to `board`.
|
||||
*/
|
||||
export function getSectionOnBoard(
|
||||
{ sx, sy }: SectionPosition,
|
||||
board: BoardData,
|
||||
options: {
|
||||
/**
|
||||
* Whether the section data is only used for reading.
|
||||
*
|
||||
* If `true`, this function will return an empty section configured with default values, but will not add it to the board data to save storage.
|
||||
*/
|
||||
readOnly: boolean;
|
||||
} = { readOnly: false },
|
||||
): SectionData {
|
||||
if (!board.sections[sy]) {
|
||||
if (options.readOnly) return createSection({ sx, sy }, board.config);
|
||||
board.sections[sy] = [];
|
||||
}
|
||||
|
||||
const row = board.sections[sy];
|
||||
const existing = row[sx];
|
||||
if (existing) return existing;
|
||||
|
||||
const section = createSection({ sx, sy }, board.config);
|
||||
if (!options.readOnly) row[sx] = section;
|
||||
return section;
|
||||
}
|
||||
|
||||
export function applyChangeOnBoard(change: BoardChange, board: BoardData) {
|
||||
const sPos = locateSection(change, board.config);
|
||||
const section = getSectionOnBoard(sPos, board);
|
||||
applyChange(change, section);
|
||||
}
|
||||
17
src/logic/character.ts
Normal file
17
src/logic/character.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { unicodeWidth } from "@std/cli/unicode-width";
|
||||
|
||||
const segmenter = /*#__PURE__*/ new Intl.Segmenter("en", {
|
||||
granularity: "grapheme",
|
||||
});
|
||||
|
||||
export function getCharacterWidth(ch: string): number {
|
||||
const segments = [...segmenter.segment(ch)];
|
||||
if (segments.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one grapheme cluster, got ${segments.length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Properly fix this.
|
||||
return Math.min(unicodeWidth(ch), 2);
|
||||
}
|
||||
127
src/logic/render.ts
Normal file
127
src/logic/render.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import type { BoardData, BoardRegion } from "../types/board";
|
||||
import type { BoardRender } from "../types/render";
|
||||
import { getSectionOnBoard, locateSection } from "./board";
|
||||
|
||||
export function render(data: BoardData): BoardRender {
|
||||
const totalLineCount = data.config.sectionHeight * data.config.ySections;
|
||||
const lineLength = data.config.sectionWidth * data.config.xSections;
|
||||
|
||||
const chLines: string[][] = Array(totalLineCount);
|
||||
const colorLines: string[][] = Array(totalLineCount);
|
||||
const bgColorLines: string[][] = Array(totalLineCount);
|
||||
const widthLines: number[][] = Array(totalLineCount);
|
||||
|
||||
for (let y = 0; y < totalLineCount; y++) {
|
||||
const chLine: string[] = [];
|
||||
const colorLine: string[] = [];
|
||||
const bgColorLine: string[] = [];
|
||||
const widthLine: number[] = [];
|
||||
|
||||
let charsToSkip = 0;
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
if (charsToSkip > 0) {
|
||||
charsToSkip--;
|
||||
continue;
|
||||
}
|
||||
|
||||
const sPos = locateSection({ x, y }, data.config);
|
||||
const section = getSectionOnBoard(sPos, data, { readOnly: true });
|
||||
const xInSection = x % data.config.sectionWidth;
|
||||
const yInSection = y % data.config.sectionHeight;
|
||||
|
||||
const cCh = section.ch[yInSection]?.[xInSection] ?? " ";
|
||||
const cCo = section.color[yInSection]?.[xInSection] ?? "";
|
||||
const cBg = section.bgColor[yInSection]?.[xInSection] ?? "";
|
||||
const cWd = section.width[yInSection]?.[xInSection] ?? 1;
|
||||
|
||||
chLine.push(cCh);
|
||||
colorLine.push(cCo);
|
||||
bgColorLine.push(cBg);
|
||||
widthLine.push(cWd);
|
||||
charsToSkip += cWd - 1;
|
||||
}
|
||||
|
||||
chLines[y] = chLine;
|
||||
colorLines[y] = colorLine;
|
||||
bgColorLines[y] = bgColorLine;
|
||||
widthLines[y] = widthLine;
|
||||
}
|
||||
|
||||
return {
|
||||
w: lineLength,
|
||||
h: totalLineCount,
|
||||
ch: chLines.flat(),
|
||||
color: colorLines.flat(),
|
||||
bg_color: bgColorLines.flat(),
|
||||
width: widthLines.flat(),
|
||||
};
|
||||
}
|
||||
|
||||
export function cropRender(
|
||||
render: BoardRender,
|
||||
region: BoardRegion,
|
||||
): BoardRender {
|
||||
const ch: string[] = [];
|
||||
const color: string[] = [];
|
||||
const bg_color: string[] = [];
|
||||
const width: number[] = [];
|
||||
|
||||
const regionEndX = region.x + region.width;
|
||||
const regionEndY = region.y + region.height;
|
||||
|
||||
let srcIdx = 0;
|
||||
let displayX = 0;
|
||||
let displayY = 0;
|
||||
|
||||
while (srcIdx < render.ch.length) {
|
||||
const cCh = render.ch[srcIdx];
|
||||
const cCo = render.color[srcIdx];
|
||||
const cBg = render.bg_color[srcIdx];
|
||||
const cWd = render.width[srcIdx];
|
||||
|
||||
if (
|
||||
typeof cCh !== "string" ||
|
||||
typeof cCo !== "string" ||
|
||||
typeof cBg !== "string" ||
|
||||
typeof cWd !== "number"
|
||||
) {
|
||||
throw new Error("Invalid render data");
|
||||
}
|
||||
|
||||
const charEndX = displayX + cWd;
|
||||
|
||||
if (displayY >= region.y && displayY < regionEndY) {
|
||||
// Check if this character overlaps with the crop region horizontally
|
||||
if (charEndX > region.x && displayX < regionEndX) {
|
||||
// Clamp the width to fit within the crop region
|
||||
const clampedStartX = Math.max(displayX, region.x);
|
||||
const clampedEndX = Math.min(charEndX, regionEndX);
|
||||
const clampedWidth = clampedEndX - clampedStartX;
|
||||
|
||||
ch.push(cCh);
|
||||
color.push(cCo);
|
||||
bg_color.push(cBg);
|
||||
width.push(clampedWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Advance display position
|
||||
displayX += cWd;
|
||||
if (displayX >= render.w) {
|
||||
displayX = 0;
|
||||
displayY++;
|
||||
}
|
||||
|
||||
srcIdx++;
|
||||
}
|
||||
|
||||
return {
|
||||
w: region.width,
|
||||
h: region.height,
|
||||
ch,
|
||||
color,
|
||||
bg_color,
|
||||
width,
|
||||
};
|
||||
}
|
||||
74
src/logic/section.ts
Normal file
74
src/logic/section.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { getCharacterWidth } from "../mod";
|
||||
import type { BoardConfig } from "../types/board";
|
||||
import type { BoardChange } from "../types/change";
|
||||
import type { SectionData, SectionPosition } from "../types/section";
|
||||
|
||||
export function createSection(
|
||||
{ sx, sy }: SectionPosition,
|
||||
boardConfig: BoardConfig,
|
||||
): SectionData {
|
||||
if (boardConfig.sectionWidth % 2 !== 0) {
|
||||
throw new Error(
|
||||
"sectionWidth must be multiple of 2 (least common multiples of all character widths)",
|
||||
);
|
||||
}
|
||||
|
||||
const offsetX = sx * boardConfig.sectionWidth;
|
||||
const offsetY = sy * boardConfig.sectionHeight;
|
||||
|
||||
const ch: string[][] = Array(boardConfig.sectionHeight)
|
||||
.fill([])
|
||||
.map(() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultCh));
|
||||
const color: string[][] = Array(boardConfig.sectionHeight)
|
||||
.fill([])
|
||||
.map(() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultColor));
|
||||
const bgColor: string[][] = Array(boardConfig.sectionHeight)
|
||||
.fill([])
|
||||
.map(() =>
|
||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultBgColor),
|
||||
);
|
||||
const width: number[][] = Array(boardConfig.sectionHeight)
|
||||
.fill([])
|
||||
.map(() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultWidth));
|
||||
|
||||
return { offsetX, offsetY, ch, color, bgColor, width };
|
||||
}
|
||||
|
||||
export function applyChange(change: BoardChange, section: SectionData) {
|
||||
const xInSection = change.x - section.offsetX;
|
||||
const yInSection = change.y - section.offsetY;
|
||||
|
||||
const row0 = section.ch[0];
|
||||
const validX =
|
||||
xInSection >= 0 && row0 !== undefined && xInSection < row0.length;
|
||||
const validY = yInSection >= 0 && yInSection < section.ch.length;
|
||||
|
||||
const chRow = section.ch[yInSection];
|
||||
const widthRow = section.width[yInSection];
|
||||
const colorRow = section.color[yInSection];
|
||||
const bgColorRow = section.bgColor[yInSection];
|
||||
|
||||
const hasRowsForY =
|
||||
chRow !== undefined &&
|
||||
widthRow !== undefined &&
|
||||
colorRow !== undefined &&
|
||||
bgColorRow !== undefined;
|
||||
|
||||
if (!validX || !validY || !hasRowsForY) {
|
||||
throw new Error("Change does not belong to this section");
|
||||
}
|
||||
|
||||
if (change.ch) {
|
||||
const chWidth = getCharacterWidth(change.ch);
|
||||
const xCharacterOffset = xInSection % chWidth;
|
||||
const offsetAdjustedXInSection = xInSection - xCharacterOffset;
|
||||
chRow[offsetAdjustedXInSection] = change.ch;
|
||||
widthRow[offsetAdjustedXInSection] = chWidth;
|
||||
}
|
||||
if (change.color) {
|
||||
colorRow[xInSection] = change.color;
|
||||
}
|
||||
if (change.bg_color) {
|
||||
bgColorRow[xInSection] = change.bg_color;
|
||||
}
|
||||
}
|
||||
9
src/mod.ts
Normal file
9
src/mod.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export * from "./types/board";
|
||||
export * from "./types/change";
|
||||
export * from "./types/render";
|
||||
export * from "./types/section";
|
||||
|
||||
export * from "./logic/board";
|
||||
export * from "./logic/character";
|
||||
export * from "./logic/render";
|
||||
export * from "./logic/section";
|
||||
|
|
@ -1,26 +1,5 @@
|
|||
import type { SectionData } from "./section.ts";
|
||||
|
||||
/**
|
||||
* A compact form to represent the whole game board.
|
||||
*
|
||||
* Note that this form is not designed for manipulation. It's designed for transmission and rendering, and can not be converted back to `BoardData` as all "over-shadowed" characters are removed.
|
||||
*/
|
||||
export interface FullBoard {
|
||||
/** The total width of the board, in display characters (`ch`). */
|
||||
w: number;
|
||||
/** The total height of the board, in `ch`. */
|
||||
h: number;
|
||||
|
||||
/** Compact array of characters on board. */
|
||||
ch: string[];
|
||||
/** Compact array of color, for each character. */
|
||||
color: string[];
|
||||
/** Compact array of background color, for each character. */
|
||||
bg_color: string[];
|
||||
/** Compact array of width indicator for each character. */
|
||||
width: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A structure defining a character position on board.
|
||||
*
|
||||
|
|
@ -33,6 +12,14 @@ export interface CharacterPosition {
|
|||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A structure defining a region on board.
|
||||
*/
|
||||
export interface BoardRegion extends CharacterPosition {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface BoardConfig {
|
||||
xSections: number;
|
||||
ySections: number;
|
||||
20
src/types/render.ts
Normal file
20
src/types/render.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* A compact form to represent a render of a board or a part of.
|
||||
*
|
||||
* Note that this form is not designed for manipulation. It's designed for transmission and rendering, and can not be converted back to `BoardData` as all "over-shadowed" characters are removed.
|
||||
*/
|
||||
export interface BoardRender {
|
||||
/** The total width of the render, in display characters (`ch`). */
|
||||
w: number;
|
||||
/** The total height of the render, in `ch`. */
|
||||
h: number;
|
||||
|
||||
/** Compact array of characters on board. */
|
||||
ch: string[];
|
||||
/** Compact array of color, for each character. */
|
||||
color: string[];
|
||||
/** Compact array of background color, for each character. */
|
||||
bg_color: string[];
|
||||
/** Compact array of width indicator for each character. */
|
||||
width: number[];
|
||||
}
|
||||
|
|
@ -1,22 +1,14 @@
|
|||
import {
|
||||
assert,
|
||||
assertEquals,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
createBoard,
|
||||
getSectionOnBoard,
|
||||
renderFullBoard,
|
||||
} from "../logic/board.ts";
|
||||
import type { BoardData } from "../types/board.ts";
|
||||
import { checkFullBoard } from "./checkFullBoard.ts";
|
||||
import { locateSection } from "../logic/board.ts";
|
||||
import { applyChangeOnBoard } from "../logic/board.ts";
|
||||
import { createBoard, getSectionOnBoard } from "../src/logic/board";
|
||||
import type { BoardData } from "../src/types/board";
|
||||
import { locateSection } from "../src/logic/board";
|
||||
import { applyChangeOnBoard } from "../src/logic/board";
|
||||
|
||||
Deno.test("board", async (t) => {
|
||||
describe("board", () => {
|
||||
let board: BoardData | undefined;
|
||||
|
||||
await t.step("createBoard", () => {
|
||||
it("createBoard", () => {
|
||||
board = createBoard({
|
||||
xSections: 3,
|
||||
ySections: 3,
|
||||
|
|
@ -29,92 +21,89 @@ Deno.test("board", async (t) => {
|
|||
});
|
||||
|
||||
// Sections are created on demand.
|
||||
assertEquals(board.sections.length, 0);
|
||||
expect(board.sections.length).toBe(0);
|
||||
});
|
||||
|
||||
await t.step("locateSection", () => {
|
||||
assert(board);
|
||||
it("locateSection", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
const { sx, sy } = locateSection({ x: 0, y: 0 }, board.config);
|
||||
assertEquals(sx, 0);
|
||||
assertEquals(sy, 0);
|
||||
const { sx, sy } = locateSection({ x: 0, y: 0 }, board!.config);
|
||||
expect(sx).toBe(0);
|
||||
expect(sy).toBe(0);
|
||||
|
||||
const { sx: sx2, sy: sy2 } = locateSection({ x: 4, y: 0 }, board.config);
|
||||
assertEquals(sx2, 1);
|
||||
assertEquals(sy2, 0);
|
||||
const { sx: sx2, sy: sy2 } = locateSection({ x: 4, y: 0 }, board!.config);
|
||||
expect(sx2).toBe(1);
|
||||
expect(sy2).toBe(0);
|
||||
});
|
||||
|
||||
await t.step("applyChangeOnBoard", () => {
|
||||
assert(board);
|
||||
it("applyChangeOnBoard", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
applyChangeOnBoard({ x: 0, y: 0, ch: "A" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 0, ch: "B" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 3, ch: "C" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 3, ch: "D" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 0, ch: "A" }, board!);
|
||||
applyChangeOnBoard({ x: 4, y: 0, ch: "B" }, board!);
|
||||
applyChangeOnBoard({ x: 0, y: 3, ch: "C" }, board!);
|
||||
applyChangeOnBoard({ x: 4, y: 3, ch: "D" }, board!);
|
||||
applyChangeOnBoard({ x: 5, y: 3, ch: "E" }, board!);
|
||||
|
||||
assertEquals(board.sections[0][0].ch[0][0], "A");
|
||||
assertEquals(board.sections[0][1].ch[0][0], "B");
|
||||
assertEquals(board.sections[1][0].ch[0][0], "C");
|
||||
assertEquals(board.sections[1][1].ch[0][0], "D");
|
||||
expect(board!.sections[0]![0]!.ch[0]![0]).toBe("A");
|
||||
expect(board!.sections[0]![1]!.ch[0]![0]).toBe("B");
|
||||
expect(board!.sections[1]![0]!.ch[0]![0]).toBe("C");
|
||||
expect(board!.sections[1]![1]!.ch[0]).toEqual(["D", "E", " ", " "]);
|
||||
|
||||
applyChangeOnBoard({ x: 0, y: 1, ch: "你" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 2, ch: "好" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 4, ch: "嗎" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 4, ch: "嘛" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 1, ch: "你" }, board!);
|
||||
applyChangeOnBoard({ x: 4, y: 2, ch: "好" }, board!);
|
||||
applyChangeOnBoard({ x: 0, y: 4, ch: "嗎" }, board!);
|
||||
applyChangeOnBoard({ x: 4, y: 4, ch: "嘛" }, board!);
|
||||
|
||||
assertEquals(board.sections[0][0].ch[1][0], "你");
|
||||
assertEquals(board.sections[0][1].ch[2][0], "好");
|
||||
assertEquals(board.sections[1][0].ch[1][0], "嗎");
|
||||
assertEquals(board.sections[1][1].ch[2][0], "嘛");
|
||||
expect(board!.sections[0]![0]!.ch[1]![0]).toBe("你");
|
||||
expect(board!.sections[0]![1]!.ch[2]![0]).toBe("好");
|
||||
expect(board!.sections[1]![0]!.ch[1]![0]).toBe("嗎");
|
||||
expect(board!.sections[1]![1]!.ch[1]).toEqual(["嘛", " ", " ", " "]);
|
||||
|
||||
applyChangeOnBoard({ x: 5, y: 4, ch: "啊" }, board!);
|
||||
expect(board!.sections[1]![1]!.ch[1]).toEqual(["啊", " ", " ", " "]);
|
||||
});
|
||||
|
||||
await t.step("getSectionOnBoard: existing section", () => {
|
||||
assert(board);
|
||||
it("getSectionOnBoard: existing section", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
const section = getSectionOnBoard({ sx: 1, sy: 1 }, board, {
|
||||
const section = getSectionOnBoard({ sx: 1, sy: 1 }, board!, {
|
||||
readOnly: true,
|
||||
});
|
||||
assertEquals(section.ch[0][0], "嘛");
|
||||
assertEquals(section.color[0][0], "F");
|
||||
assertEquals(section.bgColor[0][0], "0");
|
||||
assertEquals(section.width[0][0], 2);
|
||||
expect(section.ch[0]).toEqual(["D", "E", " ", " "]);
|
||||
expect(section.color[0]![0]).toBe("F");
|
||||
expect(section.bgColor[0]![0]).toBe("0");
|
||||
expect(section.width[0]).toEqual([1, 1, 1, 1]);
|
||||
});
|
||||
|
||||
await t.step("getSectionOnBoard: non-existing row", () => {
|
||||
assert(board);
|
||||
it("getSectionOnBoard: non-existing row", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
const section = getSectionOnBoard({ sx: 1, sy: 2 }, board, {
|
||||
const section = getSectionOnBoard({ sx: 1, sy: 2 }, board!, {
|
||||
readOnly: true,
|
||||
});
|
||||
assertEquals(section.ch[0][0], " ");
|
||||
assertEquals(section.color[0][0], "F");
|
||||
assertEquals(section.bgColor[0][0], "0");
|
||||
assertEquals(section.width[0][0], 1);
|
||||
expect(section.ch[0]![0]).toBe(" ");
|
||||
expect(section.color[0]![0]).toBe("F");
|
||||
expect(section.bgColor[0]![0]).toBe("0");
|
||||
expect(section.width[0]![0]).toBe(1);
|
||||
});
|
||||
|
||||
await t.step("getSectionOnBoard: non-existing section", () => {
|
||||
assert(board);
|
||||
it("getSectionOnBoard: non-existing section", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
const section = getSectionOnBoard({ sx: 2, sy: 1 }, board, {
|
||||
const section = getSectionOnBoard({ sx: 2, sy: 1 }, board!, {
|
||||
readOnly: true,
|
||||
});
|
||||
assertEquals(section.ch[0][0], " ");
|
||||
assertEquals(section.color[0][0], "F");
|
||||
assertEquals(section.bgColor[0][0], "0");
|
||||
assertEquals(section.width[0][0], 1);
|
||||
expect(section.ch[0]![0]).toBe(" ");
|
||||
expect(section.color[0]![0]).toBe("F");
|
||||
expect(section.bgColor[0]![0]).toBe("0");
|
||||
expect(section.width[0]![0]).toBe(1);
|
||||
});
|
||||
|
||||
await t.step("renderFullBoard", () => {
|
||||
assert(board);
|
||||
it("on-demand creation: only changed sections are saved", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
const rendered = renderFullBoard(board);
|
||||
checkFullBoard(rendered);
|
||||
});
|
||||
|
||||
await t.step("on-demand creation: only changed sections are saved", () => {
|
||||
assert(board);
|
||||
|
||||
assertEquals(board.sections.length, 2);
|
||||
assertEquals(board.sections[0].length, 2);
|
||||
expect(board!.sections[2]).toBeUndefined();
|
||||
expect(board!.sections[0]![2]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,39 +1,40 @@
|
|||
import {
|
||||
assertEquals,
|
||||
assertThrows,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { it, expect } from "vitest";
|
||||
|
||||
import { getCharacterWidth } from "../mod.ts";
|
||||
import { getCharacterWidth } from "../src/mod";
|
||||
|
||||
Deno.test("getCharacterWidth ASCII", () => {
|
||||
assertEquals(getCharacterWidth("a"), 1);
|
||||
assertEquals(getCharacterWidth("A"), 1);
|
||||
assertEquals(getCharacterWidth("1"), 1);
|
||||
assertEquals(getCharacterWidth("@"), 1);
|
||||
assertEquals(getCharacterWidth(" "), 1);
|
||||
it("getCharacterWidth ASCII", () => {
|
||||
expect(getCharacterWidth("a")).toBe(1);
|
||||
expect(getCharacterWidth("A")).toBe(1);
|
||||
expect(getCharacterWidth("1")).toBe(1);
|
||||
expect(getCharacterWidth("@")).toBe(1);
|
||||
expect(getCharacterWidth(" ")).toBe(1);
|
||||
|
||||
assertThrows(() => getCharacterWidth(""));
|
||||
assertThrows(() => getCharacterWidth("ab"));
|
||||
expect(() => getCharacterWidth("")).toThrow();
|
||||
expect(() => getCharacterWidth("ab")).toThrow();
|
||||
});
|
||||
|
||||
Deno.test("getCharacterWidth CJK", () => {
|
||||
assertEquals(getCharacterWidth("你"), 2);
|
||||
assertEquals(getCharacterWidth("好"), 2);
|
||||
assertEquals(getCharacterWidth("吗"), 2);
|
||||
it("getCharacterWidth CJK", () => {
|
||||
expect(getCharacterWidth("你")).toBe(2);
|
||||
expect(getCharacterWidth("好")).toBe(2);
|
||||
expect(getCharacterWidth("吗")).toBe(2);
|
||||
|
||||
assertEquals(getCharacterWidth("ガ"), 2);
|
||||
assertEquals(getCharacterWidth("ギ"), 2);
|
||||
assertEquals(getCharacterWidth("グ"), 2);
|
||||
assertEquals(getCharacterWidth("ソ"), 2);
|
||||
expect(getCharacterWidth("ガ")).toBe(2);
|
||||
expect(getCharacterWidth("ギ")).toBe(2);
|
||||
expect(getCharacterWidth("グ")).toBe(2);
|
||||
expect(getCharacterWidth("ソ")).toBe(2);
|
||||
|
||||
assertThrows(() => getCharacterWidth("?"));
|
||||
assertThrows(() => getCharacterWidth("!"));
|
||||
assertThrows(() => getCharacterWidth("你好"));
|
||||
assertThrows(() => getCharacterWidth("ヨスガノ"));
|
||||
expect(getCharacterWidth("?")).toBe(2);
|
||||
expect(getCharacterWidth("!")).toBe(2);
|
||||
expect(() => getCharacterWidth("你好")).toThrow();
|
||||
expect(() => getCharacterWidth("ヨスガノ")).toThrow();
|
||||
});
|
||||
|
||||
Deno.test("getCharacterWidth previously faulty cases", () => {
|
||||
assertEquals(getCharacterWidth("𤲶"), 2);
|
||||
|
||||
assertThrows(() => getCharacterWidth("𤲶"[0]));
|
||||
it("getCharacterWidth Emoji", () => {
|
||||
expect(getCharacterWidth("👋")).toBe(2);
|
||||
expect(getCharacterWidth("🌲️")).toBe(2);
|
||||
expect(getCharacterWidth("👨👩👧👦")).toBe(2);
|
||||
});
|
||||
|
||||
it("getCharacterWidth previously faulty cases", () => {
|
||||
expect(getCharacterWidth("𤲶")).toBe(2);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,30 +1,42 @@
|
|||
import { getCharacterWidth } from "../logic/character.ts";
|
||||
import type { FullBoard } from "../types/board.ts";
|
||||
|
||||
function isCorrectWidth(cWd: number, cCh: string): boolean {
|
||||
return getCharacterWidth(cCh) === cWd;
|
||||
}
|
||||
import { getCharacterWidth } from "../src/logic/character";
|
||||
import type { BoardRender } from "../src/types/render";
|
||||
|
||||
function isValidColor(color: string): boolean {
|
||||
return /^[0-9A-F]$/.test(color);
|
||||
}
|
||||
|
||||
export function checkFullBoard(board: FullBoard) {
|
||||
interface Options {
|
||||
/**
|
||||
* Whether to allow the width of a character to be narrower than the "correct" width.
|
||||
*
|
||||
* For partial renders, some wide characters may be clipped to a narrower width. This option allows for that.
|
||||
*/
|
||||
allowsNarrowerWidth?: boolean;
|
||||
}
|
||||
|
||||
export function checkBoardRender(render: BoardRender, options?: Options) {
|
||||
let chLine = "";
|
||||
let colorLine = "";
|
||||
let bgColorLine = "";
|
||||
let widthLine = "";
|
||||
let lines = 0;
|
||||
const ch = [...board.ch];
|
||||
const ch = [...render.ch];
|
||||
const chLength = ch.length;
|
||||
|
||||
let unsafeCurrentOffset = 0;
|
||||
|
||||
function isCorrectWidth(cWd: number, cCh: string): boolean {
|
||||
const correctWidth = getCharacterWidth(cCh);
|
||||
return options?.allowsNarrowerWidth
|
||||
? cWd <= correctWidth && cWd > 0
|
||||
: cWd === correctWidth;
|
||||
}
|
||||
|
||||
for (let i = 0; i < chLength; i++) {
|
||||
const cCh = ch[i];
|
||||
const cCo = board.color[i];
|
||||
const cBg = board.bg_color[i];
|
||||
const cWd = board.width[i];
|
||||
const cCo = render.color[i];
|
||||
const cBg = render.bg_color[i];
|
||||
const cWd = render.width[i];
|
||||
|
||||
const printSituation = () => {
|
||||
console.error(
|
||||
|
|
@ -47,6 +59,11 @@ export function checkFullBoard(board: FullBoard) {
|
|||
console.error("width: ", widthLine);
|
||||
};
|
||||
|
||||
if (typeof cCh !== "string") {
|
||||
printSituation();
|
||||
throw new Error("cCh is not string");
|
||||
}
|
||||
|
||||
if (!isValidColor(cCo) || !isValidColor(cBg)) {
|
||||
printSituation();
|
||||
throw new Error("cCo or cBg is not valid");
|
||||
|
|
@ -63,7 +80,7 @@ export function checkFullBoard(board: FullBoard) {
|
|||
widthLine += String(cWd).padEnd(cWd);
|
||||
unsafeCurrentOffset += cCh.length;
|
||||
|
||||
if (colorLine.length === board.w) {
|
||||
if (colorLine.length === render.w) {
|
||||
lines++;
|
||||
chLine = "";
|
||||
colorLine = "";
|
||||
|
|
@ -72,5 +89,5 @@ export function checkFullBoard(board: FullBoard) {
|
|||
}
|
||||
}
|
||||
|
||||
if (lines !== board.h) throw new Error("board height error");
|
||||
if (lines !== render.h) throw new Error("board height error");
|
||||
}
|
||||
307
tests/render.test.ts
Normal file
307
tests/render.test.ts
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { render, cropRender } from "../src/logic/render";
|
||||
import { checkBoardRender } from "./checkBoardRender";
|
||||
import { applyChangeOnBoard, createBoard } from "../src/logic/board";
|
||||
import type { BoardData } from "../src/types/board";
|
||||
import type { BoardRender } from "../src/types/render";
|
||||
|
||||
const board: BoardData = createBoard({
|
||||
xSections: 1,
|
||||
ySections: 1,
|
||||
sectionWidth: 10,
|
||||
sectionHeight: 5,
|
||||
defaultCh: " ",
|
||||
defaultColor: "F",
|
||||
defaultBgColor: "0",
|
||||
defaultWidth: 1,
|
||||
});
|
||||
|
||||
// Add CJK characters (width 2)
|
||||
applyChangeOnBoard({ x: 0, y: 0, ch: "你" }, board);
|
||||
applyChangeOnBoard({ x: 2, y: 0, ch: "好" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 0, ch: "世" }, board);
|
||||
applyChangeOnBoard({ x: 6, y: 0, ch: "界" }, board);
|
||||
|
||||
// Add cell with non-default foreground color
|
||||
applyChangeOnBoard({ x: 0, y: 1, ch: "A" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 1, color: "C" }, board);
|
||||
|
||||
// Add cell with non-default background color
|
||||
applyChangeOnBoard({ x: 2, y: 1, ch: "B" }, board);
|
||||
applyChangeOnBoard({ x: 2, y: 1, bg_color: "A" }, board);
|
||||
|
||||
// Add cell with both non-default foreground and background colors
|
||||
applyChangeOnBoard({ x: 4, y: 1, ch: "C" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 1, color: "9" }, board);
|
||||
applyChangeOnBoard({ x: 4, y: 1, bg_color: "E" }, board);
|
||||
|
||||
// Add CJK character with custom colors
|
||||
applyChangeOnBoard({ x: 0, y: 2, ch: "中" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 2, color: "D" }, board);
|
||||
applyChangeOnBoard({ x: 0, y: 2, bg_color: "B" }, board);
|
||||
|
||||
describe("render", () => {
|
||||
it("render", () => {
|
||||
expect(board).toBeDefined();
|
||||
|
||||
const rendered = render(board!);
|
||||
checkBoardRender(rendered);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cropRender", () => {
|
||||
// Helper to create a simple render for testing
|
||||
function createTestRender(): BoardRender {
|
||||
// Create a 10x5 board with:
|
||||
// Row 0: "你好世界 " (4 CJK chars = 8 display cols + 2 spaces)
|
||||
// Row 1: "A B C " (ASCII with spaces)
|
||||
// Row 2: "中 " (1 CJK char + 8 spaces)
|
||||
// Row 3: " " (all spaces)
|
||||
// Row 4: " " (all spaces)
|
||||
return render(board);
|
||||
}
|
||||
|
||||
describe("basic cropping with width-1 characters", () => {
|
||||
it("crops a region containing only width-1 characters", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop row 1 (ASCII chars), columns 0-5
|
||||
const cropped = cropRender(rendered, { x: 0, y: 1, width: 5, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(5);
|
||||
expect(cropped.h).toBe(1);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("crops middle section of width-1 characters", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop row 1, columns 1-4 (should get " B C")
|
||||
const cropped = cropRender(rendered, { x: 1, y: 1, width: 4, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(4);
|
||||
expect(cropped.h).toBe(1);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("wide characters (width 2)", () => {
|
||||
it("includes wide character entirely within crop region", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop row 0, columns 0-4 (should include "你好")
|
||||
const cropped = cropRender(rendered, { x: 0, y: 0, width: 4, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(4);
|
||||
expect(cropped.h).toBe(1);
|
||||
expect(cropped.ch).toContain("你");
|
||||
expect(cropped.ch).toContain("好");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("handles wide character at crop start boundary (character starts before region)", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop starting at x=1 - "你" starts at x=0 and extends to x=2
|
||||
// The crop should include "你" with clamped width of 1
|
||||
const cropped = cropRender(rendered, { x: 1, y: 0, width: 3, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(3);
|
||||
expect(cropped.h).toBe(1);
|
||||
// "你" should be included but with width clamped to 1 (only 1 col visible)
|
||||
expect(cropped.ch).toContain("你");
|
||||
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||
});
|
||||
|
||||
it("handles wide character at crop end boundary (character extends beyond region)", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop ending at x=3 - "好" starts at x=2 and extends to x=4
|
||||
// The crop should include "好" with clamped width of 1
|
||||
const cropped = cropRender(rendered, { x: 0, y: 0, width: 3, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(3);
|
||||
expect(cropped.h).toBe(1);
|
||||
// "你" should be included with full width, "好" should be clamped
|
||||
expect(cropped.ch).toContain("你");
|
||||
expect(cropped.ch).toContain("好");
|
||||
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||
});
|
||||
|
||||
it("handles wide character with both boundaries clamped", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop from x=1 to x=2 (width 1) - only partial view of "你"
|
||||
const cropped = cropRender(rendered, { x: 1, y: 0, width: 1, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(1);
|
||||
expect(cropped.h).toBe(1);
|
||||
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||
});
|
||||
|
||||
it("excludes wide character entirely outside crop region", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop row 0, columns 8-10 (should only get spaces, no CJK)
|
||||
const cropped = cropRender(rendered, { x: 8, y: 0, width: 2, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(2);
|
||||
expect(cropped.h).toBe(1);
|
||||
// Should only contain spaces
|
||||
expect(cropped.ch.every((c) => c === " ")).toBe(true);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multi-row cropping", () => {
|
||||
it("crops multiple rows correctly", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop rows 0-2, columns 0-4
|
||||
const cropped = cropRender(rendered, { x: 0, y: 0, width: 4, height: 3 });
|
||||
|
||||
expect(cropped.w).toBe(4);
|
||||
expect(cropped.h).toBe(3);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("crops rows from middle of board", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop rows 1-3, columns 2-6
|
||||
const cropped = cropRender(rendered, { x: 2, y: 1, width: 4, height: 3 });
|
||||
|
||||
expect(cropped.w).toBe(4);
|
||||
expect(cropped.h).toBe(3);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("handles wide characters across multiple rows", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop rows 0 and 2 which both have CJK characters
|
||||
const cropped = cropRender(rendered, { x: 0, y: 0, width: 4, height: 3 });
|
||||
|
||||
expect(cropped.w).toBe(4);
|
||||
expect(cropped.h).toBe(3);
|
||||
expect(cropped.ch).toContain("你");
|
||||
expect(cropped.ch).toContain("中");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full board crop", () => {
|
||||
it("returns equivalent data when cropping entire board", () => {
|
||||
const rendered = createTestRender();
|
||||
const cropped = cropRender(rendered, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: rendered.w,
|
||||
height: rendered.h,
|
||||
});
|
||||
|
||||
expect(cropped.w).toBe(rendered.w);
|
||||
expect(cropped.h).toBe(rendered.h);
|
||||
expect(cropped.ch).toEqual(rendered.ch);
|
||||
expect(cropped.color).toEqual(rendered.color);
|
||||
expect(cropped.bg_color).toEqual(rendered.bg_color);
|
||||
expect(cropped.width).toEqual(rendered.width);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles zero-width region", () => {
|
||||
const rendered = createTestRender();
|
||||
const cropped = cropRender(rendered, { x: 0, y: 0, width: 0, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(0);
|
||||
expect(cropped.h).toBe(1);
|
||||
expect(cropped.ch).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles zero-height region", () => {
|
||||
const rendered = createTestRender();
|
||||
const cropped = cropRender(rendered, { x: 0, y: 0, width: 5, height: 0 });
|
||||
|
||||
expect(cropped.w).toBe(5);
|
||||
expect(cropped.h).toBe(0);
|
||||
expect(cropped.ch).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles single cell crop", () => {
|
||||
const rendered = createTestRender();
|
||||
const cropped = cropRender(rendered, { x: 0, y: 1, width: 1, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(1);
|
||||
expect(cropped.h).toBe(1);
|
||||
expect(cropped.ch.length).toBe(1);
|
||||
expect(cropped.ch[0]).toBe("A");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("handles crop at bottom-right corner", () => {
|
||||
const rendered = createTestRender();
|
||||
const cropped = cropRender(rendered, { x: 8, y: 4, width: 2, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(2);
|
||||
expect(cropped.h).toBe(1);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("handles crop region starting beyond first row", () => {
|
||||
const rendered = createTestRender();
|
||||
const cropped = cropRender(rendered, { x: 0, y: 3, width: 5, height: 2 });
|
||||
|
||||
expect(cropped.w).toBe(5);
|
||||
expect(cropped.h).toBe(2);
|
||||
// Should only contain spaces (rows 3-4 are empty)
|
||||
expect(cropped.ch.every((c) => c === " ")).toBe(true);
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("color preservation", () => {
|
||||
it("preserves foreground color when cropping", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop to get cell with custom foreground color (A at 0,1 with color C)
|
||||
const cropped = cropRender(rendered, { x: 0, y: 1, width: 1, height: 1 });
|
||||
|
||||
expect(cropped.color[0]).toBe("C");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("preserves background color when cropping", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop to get cell with custom background color (B at 2,1 with bg_color A)
|
||||
const cropped = cropRender(rendered, { x: 2, y: 1, width: 1, height: 1 });
|
||||
|
||||
expect(cropped.bg_color[0]).toBe("A");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("preserves both colors on CJK character when cropping", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop to get CJK with custom colors (中 at 0,2 with color D, bg_color B)
|
||||
const cropped = cropRender(rendered, { x: 0, y: 2, width: 2, height: 1 });
|
||||
|
||||
expect(cropped.ch[0]).toBe("中");
|
||||
expect(cropped.color[0]).toBe("D");
|
||||
expect(cropped.bg_color[0]).toBe("B");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("consecutive wide characters", () => {
|
||||
it("handles crop in middle of consecutive wide characters", () => {
|
||||
const rendered = createTestRender();
|
||||
// Row 0 has: 你(0-1)好(2-3)世(4-5)界(6-7)
|
||||
// Crop from x=2 to x=6 should get 好世
|
||||
const cropped = cropRender(rendered, { x: 2, y: 0, width: 4, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(4);
|
||||
expect(cropped.ch).toContain("好");
|
||||
expect(cropped.ch).toContain("世");
|
||||
checkBoardRender(cropped);
|
||||
});
|
||||
|
||||
it("handles crop splitting multiple wide characters", () => {
|
||||
const rendered = createTestRender();
|
||||
// Crop from x=1 to x=7 - should clip 你 at start and 界 at end
|
||||
const cropped = cropRender(rendered, { x: 1, y: 0, width: 6, height: 1 });
|
||||
|
||||
expect(cropped.w).toBe(6);
|
||||
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
import {
|
||||
assert,
|
||||
assertEquals,
|
||||
assertThrows,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { applyChange, createSection } from "../logic/section.ts";
|
||||
import type { SectionData } from "../types/section.ts";
|
||||
import { applyChange, createSection } from "../src/logic/section";
|
||||
import type { SectionData } from "../src/types/section";
|
||||
|
||||
Deno.test("section", async (t) => {
|
||||
describe("section", () => {
|
||||
let section: SectionData | undefined;
|
||||
|
||||
await t.step("createSection non-lcm", () => {
|
||||
assertThrows(() => {
|
||||
it("createSection non-lcm", () => {
|
||||
expect(() => {
|
||||
createSection(
|
||||
{ sx: 0, sy: 0 },
|
||||
{
|
||||
|
|
@ -25,10 +21,10 @@ Deno.test("section", async (t) => {
|
|||
defaultWidth: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
await t.step("createSection non-origin section", () => {
|
||||
it("createSection non-origin section", () => {
|
||||
section = createSection(
|
||||
{ sx: 1, sy: 1 },
|
||||
{
|
||||
|
|
@ -43,11 +39,11 @@ Deno.test("section", async (t) => {
|
|||
},
|
||||
);
|
||||
|
||||
assertEquals(section.offsetX, 4);
|
||||
assertEquals(section.offsetY, 3);
|
||||
expect(section.offsetX).toBe(4);
|
||||
expect(section.offsetY).toBe(3);
|
||||
});
|
||||
|
||||
await t.step("createSection", () => {
|
||||
it("createSection", () => {
|
||||
section = createSection(
|
||||
{ sx: 0, sy: 0 },
|
||||
{
|
||||
|
|
@ -62,8 +58,8 @@ Deno.test("section", async (t) => {
|
|||
},
|
||||
);
|
||||
|
||||
assertEquals(section.offsetX, 0);
|
||||
assertEquals(section.offsetY, 0);
|
||||
expect(section.offsetX).toBe(0);
|
||||
expect(section.offsetY).toBe(0);
|
||||
|
||||
function assertSectionContent<T>(
|
||||
content: T[][],
|
||||
|
|
@ -71,11 +67,11 @@ Deno.test("section", async (t) => {
|
|||
columnCount: number,
|
||||
value: T,
|
||||
) {
|
||||
assertEquals(content.length, rowCount);
|
||||
expect(content.length).toBe(rowCount);
|
||||
for (const row of content) {
|
||||
assertEquals(row.length, columnCount);
|
||||
expect(row.length).toBe(columnCount);
|
||||
for (const item of row) {
|
||||
assertEquals(item, value);
|
||||
expect(item).toBe(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,38 +82,44 @@ Deno.test("section", async (t) => {
|
|||
assertSectionContent(section.width, 3, 4, 1);
|
||||
});
|
||||
|
||||
await t.step("applyChange 1-width", () => {
|
||||
assert(section);
|
||||
it("applyChange 1-width", () => {
|
||||
expect(section).toBeDefined();
|
||||
|
||||
applyChange({ x: 0, y: 0, ch: "t" }, section);
|
||||
assertEquals(section.ch[0][0], "t");
|
||||
assertEquals(section.ch[0][1], " ");
|
||||
assertEquals(section.width[0][0], 1);
|
||||
applyChange({ x: 0, y: 0, ch: "t" }, section!);
|
||||
expect(section!.ch[0]).toEqual(["t", " ", " ", " "]);
|
||||
expect(section!.ch[1]).toEqual([" ", " ", " ", " "]);
|
||||
expect(section!.width[0]).toEqual([1, 1, 1, 1]);
|
||||
});
|
||||
|
||||
await t.step("applyChange 2-width at a correct position", () => {
|
||||
assert(section);
|
||||
it("applyChange 1-width at odd position", () => {
|
||||
expect(section).toBeDefined();
|
||||
|
||||
applyChange({ x: 0, y: 0, ch: "あ" }, section);
|
||||
assertEquals(section.ch[0][0], "あ");
|
||||
assertEquals(section.ch[0][1], " ");
|
||||
assertEquals(section.width[0][0], 2);
|
||||
applyChange({ x: 1, y: 0, ch: "t" }, section!);
|
||||
expect(section!.ch[0]).toEqual(["t", "t", " ", " "]);
|
||||
expect(section!.width[0]).toEqual([1, 1, 1, 1]);
|
||||
});
|
||||
|
||||
await t.step("applyChange 2-width at an alternate position", () => {
|
||||
assert(section);
|
||||
it("applyChange 2-width at a correct position", () => {
|
||||
expect(section).toBeDefined();
|
||||
|
||||
applyChange({ x: 1, y: 0, ch: "あ" }, section);
|
||||
assertEquals(section.ch[0][0], "あ");
|
||||
assertEquals(section.ch[0][1], " ");
|
||||
assertEquals(section.width[0][0], 2);
|
||||
applyChange({ x: 0, y: 0, ch: "あ" }, section!);
|
||||
expect(section!.ch[0]).toEqual(["あ", "t", " ", " "]);
|
||||
expect(section!.width[0]).toEqual([2, 1, 1, 1]);
|
||||
});
|
||||
|
||||
await t.step("applyChange incorrect section", () => {
|
||||
assertThrows(() => {
|
||||
assert(section);
|
||||
it("applyChange 2-width at an alternate position", () => {
|
||||
expect(section).toBeDefined();
|
||||
|
||||
applyChange({ x: 6, y: 3, ch: "あ" }, section);
|
||||
});
|
||||
applyChange({ x: 1, y: 0, ch: "あ" }, section!);
|
||||
expect(section!.ch[0]).toEqual(["あ", "t", " ", " "]);
|
||||
expect(section!.width[0]).toEqual([2, 1, 1, 1]);
|
||||
});
|
||||
|
||||
it("applyChange incorrect section", () => {
|
||||
expect(section).toBeDefined();
|
||||
|
||||
expect(() => {
|
||||
applyChange({ x: 6, y: 3, ch: "あ" }, section!);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
44
tsconfig.json
Normal file
44
tsconfig.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "ES2022",
|
||||
"target": "ES2022",
|
||||
"types": [],
|
||||
"moduleResolution": "bundler",
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue