Compare commits

..

No commits in common. "main" and "0.3.0" have entirely different histories.
main ... 0.3.0

26 changed files with 446 additions and 2222 deletions

41
.github/workflows/deno.yml vendored Normal file
View file

@ -0,0 +1,41 @@
# 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

View file

@ -1,7 +1,7 @@
name: Publish on JSR name: Publish on JSR
on: on:
workflow_run: workflow_run:
workflows: ["Test"] workflows: ["Deno"]
types: [completed] types: [completed]
branches: branches:
- "main" - "main"
@ -9,7 +9,6 @@ on:
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions: permissions:
contents: read contents: read
@ -18,36 +17,5 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup repo - name: Publish package
uses: actions/checkout@v4 run: npx jsr publish
- 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

View file

@ -9,21 +9,18 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- name: Setup repo - uses: actions/checkout@v4
uses: actions/checkout@v4 - name: Setup Deno
- uses: pnpm/action-setup@v4 uses: denoland/setup-deno@v2
with: with:
run_install: false deno-version: v2.x
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "24.x" node-version: "22.x"
cache: "pnpm"
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- run: pnpm install - run: deno run -A scripts/build_npm.ts
- run: pnpm build - run: cd npm && npm ci
- run: pnpm test - run: cd npm && npm publish --provenance --access public
# --no-git-checks because of https://github.com/pnpm/pnpm/issues/5894.
- run: pnpm publish --provenance --access public --no-git-checks
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -1,32 +0,0 @@
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
View file

@ -1,3 +1,2 @@
npm
.DS_Store .DS_Store
node_modules
dist

View file

@ -3,6 +3,25 @@
// For a full list of overridable settings, and general information on folder-specific settings, // 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 // see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
{ {
"formatter": "prettier", "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", "-"]
}
}
}
},
"format_on_save": "on" "format_on_save": "on"
} }

View file

@ -1,6 +1,9 @@
// Static tasks configuration.
//
// Example:
[ [
{ {
"label": "Test", "label": "Test",
"command": "pnpm test" "command": "deno test"
} }
] ]

View file

@ -1,5 +1,5 @@
The MIT License (MIT) The MIT License (MIT)
Copyright © 2025 Shibo Lyu <hi@lao.sb> Copyright © 2024 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: 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:

View file

@ -1,5 +0,0 @@
import { defineBuildConfig } from "obuild/config";
export default defineBuildConfig({
entries: ["src/mod.ts"],
});

View file

@ -1,11 +1,8 @@
{ {
"name": "@textplace/core", "name": "@textplace/core",
"version": "0.5.0", "version": "0.3.0",
"exports": "./src/mod.ts", "exports": "./mod.ts",
"imports": { "imports": {
"@std/cli": "jsr:@std/cli@1" "@deno/dnt": "jsr:@deno/dnt@^0.41.3"
},
"publish": {
"include": ["src/**/*.ts", "LICENSE", "README.md"]
} }
} }

157
deno.lock generated Normal file
View file

@ -0,0 +1,157 @@
{
"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"
]
}
}

View file

@ -106,9 +106,9 @@ export function renderFullBoard(data: BoardData): FullBoard {
return { return {
w: lineLength, w: lineLength,
h: totalLineCount, h: totalLineCount,
ch: chLines.flat(), ch: ([] as string[]).concat(...chLines).flat(),
color: colorLines.flat(), color: ([] as string[]).concat(...colorLines).flat(),
bg_color: bgColorLines.flat(), bg_color: ([] as string[]).concat(...bgColorLines).flat(),
width: widthLines.flat(), width: ([] as number[]).concat(...widthLines).flat(),
}; };
} }

21
logic/character.ts Normal file
View file

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

View file

@ -16,17 +16,17 @@ export function createSection(
const offsetX = sx * boardConfig.sectionWidth; const offsetX = sx * boardConfig.sectionWidth;
const offsetY = sy * boardConfig.sectionHeight; const offsetY = sy * boardConfig.sectionHeight;
const ch: string[][] = Array(boardConfig.sectionHeight).fill([]).map(() => const ch: string[][] = Array(boardConfig.sectionHeight).fill(
Array(boardConfig.sectionWidth).fill(boardConfig.defaultCh) Array(boardConfig.sectionWidth).fill(boardConfig.defaultCh),
); );
const color: string[][] = Array(boardConfig.sectionHeight).fill([]).map(() => const color: string[][] = Array(boardConfig.sectionHeight).fill(
Array(boardConfig.sectionWidth).fill(boardConfig.defaultColor) Array(boardConfig.sectionWidth).fill(boardConfig.defaultColor),
); );
const bgColor: string[][] = Array(boardConfig.sectionHeight).fill([]).map( const bgColor: string[][] = Array(boardConfig.sectionHeight).fill(
() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultBgColor), Array(boardConfig.sectionWidth).fill(boardConfig.defaultBgColor),
); );
const width: number[][] = Array(boardConfig.sectionHeight).fill([]).map(() => const width: number[][] = Array(boardConfig.sectionHeight).fill(
Array(boardConfig.sectionWidth).fill(boardConfig.defaultWidth) Array(boardConfig.sectionWidth).fill(boardConfig.defaultWidth),
); );
return { offsetX, offsetY, ch, color, bgColor, width }; return { offsetX, offsetY, ch, color, bgColor, width };
@ -44,7 +44,7 @@ export function applyChange(change: BoardChange, section: SectionData) {
if (change.ch) { if (change.ch) {
const chWidth = getCharacterWidth(change.ch); const chWidth = getCharacterWidth(change.ch);
const xCharacterOffset = xInSection % chWidth; const xCharacterOffset = xInSection % 2;
const offsetAdjustedXInSection = xInSection - xCharacterOffset; const offsetAdjustedXInSection = xInSection - xCharacterOffset;
section.ch[yInSection][offsetAdjustedXInSection] = change.ch; section.ch[yInSection][offsetAdjustedXInSection] = change.ch;
section.width[yInSection][offsetAdjustedXInSection] = chWidth; section.width[yInSection][offsetAdjustedXInSection] = chWidth;

View file

View file

@ -1,36 +0,0 @@
{
"name": "@textplace/core",
"version": "0.5.0",
"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"
},
"scripts": {
"test": "vitest",
"build": "obuild"
},
"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.23.0"
}

1917
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

36
scripts/build_npm.ts Normal file
View file

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

View file

@ -1,15 +0,0 @@
import { unicodeWidth } from "@std/cli/unicode-width";
const segmenter = 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);
}

View file

@ -1,19 +1,22 @@
import { describe, it, expect } from "vitest"; import {
assert,
assertEquals,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { import {
createBoard, createBoard,
getSectionOnBoard, getSectionOnBoard,
renderFullBoard, renderFullBoard,
} from "../src/logic/board.ts"; } from "../logic/board.ts";
import type { BoardData } from "../src/types/board.ts"; import type { BoardData } from "../types/board.ts";
import { checkFullBoard } from "./checkFullBoard.ts"; import { checkFullBoard } from "./checkFullBoard.ts";
import { locateSection } from "../src/logic/board.ts"; import { locateSection } from "../logic/board.ts";
import { applyChangeOnBoard } from "../src/logic/board.ts"; import { applyChangeOnBoard } from "../logic/board.ts";
describe("board", () => { Deno.test("board", async (t) => {
let board: BoardData | undefined; let board: BoardData | undefined;
it("createBoard", () => { await t.step("createBoard", () => {
board = createBoard({ board = createBoard({
xSections: 3, xSections: 3,
ySections: 3, ySections: 3,
@ -26,96 +29,92 @@ describe("board", () => {
}); });
// Sections are created on demand. // Sections are created on demand.
expect(board.sections.length).toBe(0); assertEquals(board.sections.length, 0);
}); });
it("locateSection", () => { await t.step("locateSection", () => {
expect(board).toBeDefined(); assert(board);
const { sx, sy } = locateSection({ x: 0, y: 0 }, board!.config); const { sx, sy } = locateSection({ x: 0, y: 0 }, board.config);
expect(sx).toBe(0); assertEquals(sx, 0);
expect(sy).toBe(0); assertEquals(sy, 0);
const { sx: sx2, sy: sy2 } = locateSection({ x: 4, y: 0 }, board!.config); const { sx: sx2, sy: sy2 } = locateSection({ x: 4, y: 0 }, board.config);
expect(sx2).toBe(1); assertEquals(sx2, 1);
expect(sy2).toBe(0); assertEquals(sy2, 0);
}); });
it("applyChangeOnBoard", () => { await t.step("applyChangeOnBoard", () => {
expect(board).toBeDefined(); assert(board);
applyChangeOnBoard({ x: 0, y: 0, ch: "A" }, board!); applyChangeOnBoard({ x: 0, y: 0, ch: "A" }, board);
applyChangeOnBoard({ x: 4, y: 0, ch: "B" }, board!); applyChangeOnBoard({ x: 4, y: 0, ch: "B" }, board);
applyChangeOnBoard({ x: 0, y: 3, ch: "C" }, board!); applyChangeOnBoard({ x: 0, y: 3, ch: "C" }, board);
applyChangeOnBoard({ x: 4, y: 3, ch: "D" }, board!); applyChangeOnBoard({ x: 4, y: 3, ch: "D" }, board);
applyChangeOnBoard({ x: 5, y: 3, ch: "E" }, board!);
expect(board!.sections[0][0].ch[0][0]).toBe("A"); assertEquals(board.sections[0][0].ch[0][0], "A");
expect(board!.sections[0][1].ch[0][0]).toBe("B"); assertEquals(board.sections[0][1].ch[0][0], "B");
expect(board!.sections[1][0].ch[0][0]).toBe("C"); assertEquals(board.sections[1][0].ch[0][0], "C");
expect(board!.sections[1][1].ch[0]).toEqual(["D", "E", " ", " "]); assertEquals(board.sections[1][1].ch[0][0], "D");
applyChangeOnBoard({ x: 0, y: 1, ch: "你" }, board!); applyChangeOnBoard({ x: 0, y: 1, ch: "你" }, board);
applyChangeOnBoard({ x: 4, y: 2, ch: "好" }, board!); applyChangeOnBoard({ x: 4, y: 2, ch: "好" }, board);
applyChangeOnBoard({ x: 0, y: 4, ch: "嗎" }, board!); applyChangeOnBoard({ x: 0, y: 4, ch: "嗎" }, board);
applyChangeOnBoard({ x: 4, y: 4, ch: "嘛" }, board!); applyChangeOnBoard({ x: 4, y: 4, ch: "嘛" }, board);
expect(board!.sections[0][0].ch[1][0]).toBe("你"); assertEquals(board.sections[0][0].ch[1][0], "你");
expect(board!.sections[0][1].ch[2][0]).toBe("好"); assertEquals(board.sections[0][1].ch[2][0], "好");
expect(board!.sections[1][0].ch[1][0]).toBe("嗎"); assertEquals(board.sections[1][0].ch[1][0], "嗎");
expect(board!.sections[1][1].ch[1]).toEqual(["嘛", " ", " ", " "]); assertEquals(board.sections[1][1].ch[2][0], "嘛");
applyChangeOnBoard({ x: 5, y: 4, ch: "啊" }, board!);
expect(board!.sections[1][1].ch[1]).toEqual(["啊", " ", " ", " "]);
}); });
it("getSectionOnBoard: existing section", () => { await t.step("getSectionOnBoard: existing section", () => {
expect(board).toBeDefined(); assert(board);
const section = getSectionOnBoard({ sx: 1, sy: 1 }, board!, { const section = getSectionOnBoard({ sx: 1, sy: 1 }, board, {
readOnly: true, readOnly: true,
}); });
expect(section.ch[0]).toEqual(["D", "E", " ", " "]); assertEquals(section.ch[0][0], "嘛");
expect(section.color[0][0]).toBe("F"); assertEquals(section.color[0][0], "F");
expect(section.bgColor[0][0]).toBe("0"); assertEquals(section.bgColor[0][0], "0");
expect(section.width[0]).toEqual([1, 1, 1, 1]); assertEquals(section.width[0][0], 2);
}); });
it("getSectionOnBoard: non-existing row", () => { await t.step("getSectionOnBoard: non-existing row", () => {
expect(board).toBeDefined(); assert(board);
const section = getSectionOnBoard({ sx: 1, sy: 2 }, board!, { const section = getSectionOnBoard({ sx: 1, sy: 2 }, board, {
readOnly: true, readOnly: true,
}); });
expect(section.ch[0][0]).toBe(" "); assertEquals(section.ch[0][0], " ");
expect(section.color[0][0]).toBe("F"); assertEquals(section.color[0][0], "F");
expect(section.bgColor[0][0]).toBe("0"); assertEquals(section.bgColor[0][0], "0");
expect(section.width[0][0]).toBe(1); assertEquals(section.width[0][0], 1);
}); });
it("getSectionOnBoard: non-existing section", () => { await t.step("getSectionOnBoard: non-existing section", () => {
expect(board).toBeDefined(); assert(board);
const section = getSectionOnBoard({ sx: 2, sy: 1 }, board!, { const section = getSectionOnBoard({ sx: 2, sy: 1 }, board, {
readOnly: true, readOnly: true,
}); });
expect(section.ch[0][0]).toBe(" "); assertEquals(section.ch[0][0], " ");
expect(section.color[0][0]).toBe("F"); assertEquals(section.color[0][0], "F");
expect(section.bgColor[0][0]).toBe("0"); assertEquals(section.bgColor[0][0], "0");
expect(section.width[0][0]).toBe(1); assertEquals(section.width[0][0], 1);
}); });
it("renderFullBoard", () => { await t.step("renderFullBoard", () => {
expect(board).toBeDefined(); assert(board);
const rendered = renderFullBoard(board!); const rendered = renderFullBoard(board);
checkFullBoard(rendered); checkFullBoard(rendered);
}); });
it("on-demand creation: only changed sections are saved", () => { await t.step("on-demand creation: only changed sections are saved", () => {
expect(board).toBeDefined(); assert(board);
expect(board!.sections[2]).toBeUndefined(); assertEquals(board.sections.length, 2);
expect(board!.sections[0][2]).toBeUndefined(); assertEquals(board.sections[0].length, 2);
}); });
}); });

View file

@ -1,40 +1,39 @@
import { it, expect } from "vitest"; import {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { getCharacterWidth } from "../src/mod.ts"; import { getCharacterWidth } from "../mod.ts";
it("getCharacterWidth ASCII", () => { Deno.test("getCharacterWidth ASCII", () => {
expect(getCharacterWidth("a")).toBe(1); assertEquals(getCharacterWidth("a"), 1);
expect(getCharacterWidth("A")).toBe(1); assertEquals(getCharacterWidth("A"), 1);
expect(getCharacterWidth("1")).toBe(1); assertEquals(getCharacterWidth("1"), 1);
expect(getCharacterWidth("@")).toBe(1); assertEquals(getCharacterWidth("@"), 1);
expect(getCharacterWidth(" ")).toBe(1); assertEquals(getCharacterWidth(" "), 1);
expect(() => getCharacterWidth("")).toThrow(); assertThrows(() => getCharacterWidth(""));
expect(() => getCharacterWidth("ab")).toThrow(); assertThrows(() => getCharacterWidth("ab"));
}); });
it("getCharacterWidth CJK", () => { Deno.test("getCharacterWidth CJK", () => {
expect(getCharacterWidth("你")).toBe(2); assertEquals(getCharacterWidth("你"), 2);
expect(getCharacterWidth("好")).toBe(2); assertEquals(getCharacterWidth("好"), 2);
expect(getCharacterWidth("吗")).toBe(2); assertEquals(getCharacterWidth("吗"), 2);
expect(getCharacterWidth("ガ")).toBe(2); assertEquals(getCharacterWidth("ガ"), 2);
expect(getCharacterWidth("ギ")).toBe(2); assertEquals(getCharacterWidth("ギ"), 2);
expect(getCharacterWidth("グ")).toBe(2); assertEquals(getCharacterWidth("グ"), 2);
expect(getCharacterWidth("ソ")).toBe(2); assertEquals(getCharacterWidth("ソ"), 2);
expect(getCharacterWidth("")).toBe(2); assertThrows(() => getCharacterWidth(""));
expect(getCharacterWidth("")).toBe(2); assertThrows(() => getCharacterWidth(""));
expect(() => getCharacterWidth("你好")).toThrow(); assertThrows(() => getCharacterWidth("你好"));
expect(() => getCharacterWidth("ヨスガノ")).toThrow(); assertThrows(() => getCharacterWidth("ヨスガノ"));
}); });
it("getCharacterWidth Emoji", () => { Deno.test("getCharacterWidth previously faulty cases", () => {
expect(getCharacterWidth("👋")).toBe(2); assertEquals(getCharacterWidth("𤲶"), 2);
expect(getCharacterWidth("🌲️")).toBe(2);
expect(getCharacterWidth("👨‍👩‍👧‍👦")).toBe(2);
});
it("getCharacterWidth previously faulty cases", () => { assertThrows(() => getCharacterWidth("𤲶"[0]));
expect(getCharacterWidth("𤲶")).toBe(2);
}); });

View file

@ -1,5 +1,5 @@
import { getCharacterWidth } from "../src/logic/character.ts"; import { getCharacterWidth } from "../logic/character.ts";
import type { FullBoard } from "../src/types/board.ts"; import type { FullBoard } from "../types/board.ts";
function isCorrectWidth(cWd: number, cCh: string): boolean { function isCorrectWidth(cWd: number, cCh: string): boolean {
return getCharacterWidth(cCh) === cWd; return getCharacterWidth(cCh) === cWd;
@ -47,11 +47,6 @@ export function checkFullBoard(board: FullBoard) {
console.error("width: ", widthLine); console.error("width: ", widthLine);
}; };
if (typeof cCh !== "string") {
printSituation();
throw new Error("cCh is not string");
}
if (!isValidColor(cCo) || !isValidColor(cBg)) { if (!isValidColor(cCo) || !isValidColor(cBg)) {
printSituation(); printSituation();
throw new Error("cCo or cBg is not valid"); throw new Error("cCo or cBg is not valid");

View file

@ -1,13 +1,17 @@
import { describe, it, expect } from "vitest"; import {
assert,
assertEquals,
assertThrows,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { applyChange, createSection } from "../src/logic/section.ts"; import { applyChange, createSection } from "../logic/section.ts";
import type { SectionData } from "../src/types/section.ts"; import type { SectionData } from "../types/section.ts";
describe("section", () => { Deno.test("section", async (t) => {
let section: SectionData | undefined; let section: SectionData | undefined;
it("createSection non-lcm", () => { await t.step("createSection non-lcm", () => {
expect(() => { assertThrows(() => {
createSection( createSection(
{ sx: 0, sy: 0 }, { sx: 0, sy: 0 },
{ {
@ -21,10 +25,10 @@ describe("section", () => {
defaultWidth: 1, defaultWidth: 1,
}, },
); );
}).toThrow(); });
}); });
it("createSection non-origin section", () => { await t.step("createSection non-origin section", () => {
section = createSection( section = createSection(
{ sx: 1, sy: 1 }, { sx: 1, sy: 1 },
{ {
@ -39,11 +43,11 @@ describe("section", () => {
}, },
); );
expect(section.offsetX).toBe(4); assertEquals(section.offsetX, 4);
expect(section.offsetY).toBe(3); assertEquals(section.offsetY, 3);
}); });
it("createSection", () => { await t.step("createSection", () => {
section = createSection( section = createSection(
{ sx: 0, sy: 0 }, { sx: 0, sy: 0 },
{ {
@ -58,8 +62,8 @@ describe("section", () => {
}, },
); );
expect(section.offsetX).toBe(0); assertEquals(section.offsetX, 0);
expect(section.offsetY).toBe(0); assertEquals(section.offsetY, 0);
function assertSectionContent<T>( function assertSectionContent<T>(
content: T[][], content: T[][],
@ -67,11 +71,11 @@ describe("section", () => {
columnCount: number, columnCount: number,
value: T, value: T,
) { ) {
expect(content.length).toBe(rowCount); assertEquals(content.length, rowCount);
for (const row of content) { for (const row of content) {
expect(row.length).toBe(columnCount); assertEquals(row.length, columnCount);
for (const item of row) { for (const item of row) {
expect(item).toBe(value); assertEquals(item, value);
} }
} }
} }
@ -82,44 +86,38 @@ describe("section", () => {
assertSectionContent(section.width, 3, 4, 1); assertSectionContent(section.width, 3, 4, 1);
}); });
it("applyChange 1-width", () => { await t.step("applyChange 1-width", () => {
expect(section).toBeDefined(); assert(section);
applyChange({ x: 0, y: 0, ch: "t" }, section!); applyChange({ x: 0, y: 0, ch: "t" }, section);
expect(section!.ch[0]).toEqual(["t", " ", " ", " "]); assertEquals(section.ch[0][0], "t");
expect(section!.ch[1]).toEqual([" ", " ", " ", " "]); assertEquals(section.ch[0][1], " ");
expect(section!.width[0]).toEqual([1, 1, 1, 1]); assertEquals(section.width[0][0], 1);
}); });
it("applyChange 1-width at odd position", () => { await t.step("applyChange 2-width at a correct position", () => {
expect(section).toBeDefined(); assert(section);
applyChange({ x: 1, y: 0, ch: "t" }, section!); applyChange({ x: 0, y: 0, ch: "あ" }, section);
expect(section!.ch[0]).toEqual(["t", "t", " ", " "]); assertEquals(section.ch[0][0], "あ");
expect(section!.width[0]).toEqual([1, 1, 1, 1]); assertEquals(section.ch[0][1], " ");
assertEquals(section.width[0][0], 2);
}); });
it("applyChange 2-width at a correct position", () => { await t.step("applyChange 2-width at an alternate position", () => {
expect(section).toBeDefined(); assert(section);
applyChange({ x: 0, y: 0, ch: "あ" }, section!); applyChange({ x: 1, y: 0, ch: "あ" }, section);
expect(section!.ch[0]).toEqual(["あ", "t", " ", " "]); assertEquals(section.ch[0][0], "あ");
expect(section!.width[0]).toEqual([2, 1, 1, 1]); assertEquals(section.ch[0][1], " ");
assertEquals(section.width[0][0], 2);
}); });
it("applyChange 2-width at an alternate position", () => { await t.step("applyChange incorrect section", () => {
expect(section).toBeDefined(); assertThrows(() => {
assert(section);
applyChange({ x: 1, y: 0, ch: "あ" }, section!); applyChange({ x: 6, y: 3, 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();
}); });
}); });