initial version

This commit is contained in:
Shibo Lyu 2024-04-28 13:05:28 +08:00
commit 6dfe7129c4
15 changed files with 614 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
npm
.DS_Store

14
.zed/settings.json Normal file
View file

@ -0,0 +1,14 @@
// 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
{
"languages": {
"TypeScript": {
"language_servers": ["deno", "!typescript-language-server", "!eslint"]
},
"TSX": {
"language_servers": ["deno", "!typescript-language-server", "!eslint"]
}
}
}

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
The MIT License (MIT)
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

5
deno.json Normal file
View file

@ -0,0 +1,5 @@
{
"imports": {
"@deno/dnt": "jsr:@deno/dnt@^0.41.1"
}
}

256
deno.lock generated Normal file
View file

@ -0,0 +1,256 @@
{
"version": "3",
"packages": {
"specifiers": {
"jsr:@deno/cache-dir@^0.8.0": "jsr:@deno/cache-dir@0.8.0",
"jsr:@deno/dnt@^0.41.1": "jsr:@deno/dnt@0.41.1",
"jsr:@deno/graph@^0.69.7": "jsr:@deno/graph@0.69.10",
"jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2",
"jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2",
"jsr:@std/fmt@^0.218.2": "jsr:@std/fmt@0.218.2",
"jsr:@std/fs@^0.218.2": "jsr:@std/fs@0.218.2",
"jsr:@std/io@^0.218.2": "jsr:@std/io@0.218.2",
"jsr:@std/path@^0.218.2": "jsr:@std/path@0.218.2",
"npm:@ts-morph/bootstrap@0.22": "npm:@ts-morph/bootstrap@0.22.0",
"npm:code-block-writer@^13.0.1": "npm:code-block-writer@13.0.1"
},
"jsr": {
"@deno/cache-dir@0.8.0": {
"integrity": "e87e80a404958f6350d903e6238b72afb92468378b0b32111f7a1e4916ac7fe7",
"dependencies": [
"jsr:@deno/graph@^0.69.7",
"jsr:@std/fs@^0.218.2",
"jsr:@std/io@^0.218.2"
]
},
"@deno/dnt@0.41.1": {
"integrity": "8746a773e031ae19ef43d0eece850217b76cf1d0118fdd8e059652d7023d4aff",
"dependencies": [
"jsr:@deno/cache-dir@^0.8.0",
"jsr:@std/fmt@^0.218.2",
"jsr:@std/fs@^0.218.2",
"jsr:@std/path@^0.218.2",
"npm:@ts-morph/bootstrap@0.22",
"npm:code-block-writer@^13.0.1"
]
},
"@deno/graph@0.69.10": {
"integrity": "38fe22ac5686f6ece5daeec5a4df65c6314d7d32adcc33f77917a13cfaffa26f"
},
"@std/assert@0.218.2": {
"integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf"
},
"@std/bytes@0.218.2": {
"integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670"
},
"@std/fmt@0.218.2": {
"integrity": "99526449d2505aa758b6cbef81e7dd471d8b28ec0dcb1491d122b284c548788a"
},
"@std/fs@0.218.2": {
"integrity": "dd9431453f7282e8c577cc22c9e6d036055a9a980b5549f887d6012969fabcca",
"dependencies": [
"jsr:@std/assert@^0.218.2",
"jsr:@std/path@^0.218.2"
]
},
"@std/io@0.218.2": {
"integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f",
"dependencies": [
"jsr:@std/bytes@^0.218.2"
]
},
"@std/path@0.218.2": {
"integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662",
"dependencies": [
"jsr:@std/assert@^0.218.2"
]
}
},
"npm": {
"@nodelib/fs.scandir@2.1.5": {
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dependencies": {
"@nodelib/fs.stat": "@nodelib/fs.stat@2.0.5",
"run-parallel": "run-parallel@1.2.0"
}
},
"@nodelib/fs.stat@2.0.5": {
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dependencies": {}
},
"@nodelib/fs.walk@1.2.8": {
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dependencies": {
"@nodelib/fs.scandir": "@nodelib/fs.scandir@2.1.5",
"fastq": "fastq@1.17.1"
}
},
"@ts-morph/bootstrap@0.22.0": {
"integrity": "sha512-MI5q7pid4swAlE2lcHwHRa6rcjoIMyT6fy8uuZm8BGg7DHGi/H5bQ0GMZzbk3N0r/LfStMdOYPkl+3IwvfIQ2g==",
"dependencies": {
"@ts-morph/common": "@ts-morph/common@0.22.0"
}
},
"@ts-morph/common@0.22.0": {
"integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==",
"dependencies": {
"fast-glob": "fast-glob@3.3.2",
"minimatch": "minimatch@9.0.4",
"mkdirp": "mkdirp@3.0.1",
"path-browserify": "path-browserify@1.0.1"
}
},
"balanced-match@1.0.2": {
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dependencies": {}
},
"brace-expansion@2.0.1": {
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "balanced-match@1.0.2"
}
},
"braces@3.0.2": {
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dependencies": {
"fill-range": "fill-range@7.0.1"
}
},
"code-block-writer@13.0.1": {
"integrity": "sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==",
"dependencies": {}
},
"fast-glob@3.3.2": {
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dependencies": {
"@nodelib/fs.stat": "@nodelib/fs.stat@2.0.5",
"@nodelib/fs.walk": "@nodelib/fs.walk@1.2.8",
"glob-parent": "glob-parent@5.1.2",
"merge2": "merge2@1.4.1",
"micromatch": "micromatch@4.0.5"
}
},
"fastq@1.17.1": {
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dependencies": {
"reusify": "reusify@1.0.4"
}
},
"fill-range@7.0.1": {
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dependencies": {
"to-regex-range": "to-regex-range@5.0.1"
}
},
"glob-parent@5.1.2": {
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dependencies": {
"is-glob": "is-glob@4.0.3"
}
},
"is-extglob@2.1.1": {
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dependencies": {}
},
"is-glob@4.0.3": {
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dependencies": {
"is-extglob": "is-extglob@2.1.1"
}
},
"is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dependencies": {}
},
"merge2@1.4.1": {
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dependencies": {}
},
"micromatch@4.0.5": {
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dependencies": {
"braces": "braces@3.0.2",
"picomatch": "picomatch@2.3.1"
}
},
"minimatch@9.0.4": {
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dependencies": {
"brace-expansion": "brace-expansion@2.0.1"
}
},
"mkdirp@3.0.1": {
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dependencies": {}
},
"path-browserify@1.0.1": {
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dependencies": {}
},
"picomatch@2.3.1": {
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dependencies": {}
},
"queue-microtask@1.2.3": {
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dependencies": {}
},
"reusify@1.0.4": {
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dependencies": {}
},
"run-parallel@1.2.0": {
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dependencies": {
"queue-microtask": "queue-microtask@1.2.3"
}
},
"to-regex-range@5.0.1": {
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "is-number@7.0.0"
}
}
}
},
"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.1"
]
}
}

25
logic/board.ts Normal file
View file

@ -0,0 +1,25 @@
import type { SectionData, SectionPosition } from "../types/section.ts";
import type {
BoardConfig,
BoardData,
CharacterPosition,
} from "../types/board.ts";
import { createSection } from "./section.ts";
export function createBoard(config: BoardConfig): BoardData {
const sections: SectionData[][] = Array(config.ySections).map((_, sy) =>
Array(config.xSections).map((_, sx) => createSection({ sx, sy }, config))
);
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),
};
}

20
logic/character.ts Normal file
View file

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

44
logic/section.ts Normal file
View file

@ -0,0 +1,44 @@
import { getCharacterWidth } from "../mod.ts";
import type { BoardConfig } from "../types/board.ts";
import { BoardChange } from "../types/change.ts";
import type { SectionData, SectionPosition } from "../types/section.ts";
export function createSection(
{ sx, sy }: SectionPosition,
boardConfig: BoardConfig
): SectionData {
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;
if (change.ch) {
const chWidth = getCharacterWidth(change.ch);
section.ch[yInSection][xInSection] = change.ch;
section.width[yInSection][xInSection] = chWidth;
}
if (change.color) {
section.color[yInSection][xInSection] = change.color;
}
if (change.bg_color) {
section.bgColor[yInSection][xInSection] = change.bg_color;
}
}

7
mod.ts Normal file
View file

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

31
scripts/build_npm.ts Normal file
View file

@ -0,0 +1,31 @@
import { build, emptyDir } from "@deno/dnt";
await emptyDir("./npm");
await build({
entryPoints: ["./mod.ts"],
outDir: "./npm",
shims: {
// see JS docs for overview and more options
deno: true,
},
package: {
// package.json properties
name: "@textplace/core",
version: Deno.args[0],
description: "The core logic of TextPlace.",
license: "MIT",
repository: {
type: "git",
url: "git+https://github.com/textplace/CoreTextPlace.git",
},
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");
},
});

39
tests/character.test.ts Normal file
View file

@ -0,0 +1,39 @@
import {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { getCharacterWidth } from "../mod.ts";
Deno.test("getCharacterWidth ASCII", () => {
assertEquals(getCharacterWidth("a"), 1);
assertEquals(getCharacterWidth("A"), 1);
assertEquals(getCharacterWidth("1"), 1);
assertEquals(getCharacterWidth("@"), 1);
assertEquals(getCharacterWidth(" "), 1);
assertThrows(() => getCharacterWidth(""));
assertThrows(() => getCharacterWidth("ab"));
});
Deno.test("getCharacterWidth CJK", () => {
assertEquals(getCharacterWidth("你"), 2);
assertEquals(getCharacterWidth("好"), 2);
assertEquals(getCharacterWidth("吗"), 2);
assertEquals(getCharacterWidth("ガ"), 2);
assertEquals(getCharacterWidth("ギ"), 2);
assertEquals(getCharacterWidth("グ"), 2);
assertEquals(getCharacterWidth("ソ"), 2);
assertThrows(() => getCharacterWidth(""));
assertThrows(() => getCharacterWidth(""));
assertThrows(() => getCharacterWidth("你好"));
assertThrows(() => getCharacterWidth("ヨスガノ"));
});
Deno.test("getCharacterWidth previously faulty cases", () => {
assertEquals(getCharacterWidth("𤲶"), 2);
assertThrows(() => getCharacterWidth("𤲶"[0]));
});

53
tests/section.test.ts Normal file
View file

@ -0,0 +1,53 @@
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { createSection } from "../logic/section.ts";
Deno.test("createSection", () => {
const section = createSection(
{ sx: 0, sy: 0 },
{
xSections: 2,
ySections: 2,
sectionWidth: 3,
sectionHeight: 3,
defaultCh: " ",
defaultColor: "F",
defaultBgColor: "0",
defaultWidth: 1,
}
);
assertEquals(section.offsetX, 0);
assertEquals(section.offsetY, 0);
assertEquals(section.ch.length, 3);
for (const row of section.ch) {
assertEquals(row.length, 3);
for (const ch of row) {
assertEquals(ch, " ");
}
}
assertEquals(section.color.length, 3);
for (const row of section.color) {
assertEquals(row.length, 3);
for (const color of row) {
assertEquals(color, "F");
}
}
assertEquals(section.bgColor.length, 3);
for (const row of section.bgColor) {
assertEquals(row.length, 3);
for (const bgColor of row) {
assertEquals(bgColor, "0");
}
}
assertEquals(section.width.length, 3);
for (const row of section.width) {
assertEquals(row.length, 3);
for (const width of row) {
assertEquals(width, 1);
}
}
});

57
types/board.ts Normal file
View file

@ -0,0 +1,57 @@
import { 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 string of characters on board. */
ch: string;
/** Compact string of color, for each character. */
color: string;
/** Compact string of background color, for each character. */
bg_color: string;
/** Compact string of width indicator for each character. */
width: string;
}
/**
* A structure defining a character position on board.
*
* In TextPlace, origin point (`0, 0`) is at the top-leading corner of the board. The X axis goes from leading (left) edge to trailing (right) edge while the Y axis goes from top to down.
*/
export interface CharacterPosition {
/** 0-based index on X axis. */
x: number;
/** 0-based index on Y axis. */
y: number;
}
export interface BoardConfig {
xSections: number;
ySections: number;
sectionWidth: number;
sectionHeight: number;
defaultCh: string;
defaultColor: string;
defaultBgColor: string;
defaultWidth: number;
}
export interface BoardData {
/**
* Configuration of the board.
*
* This is a static configuration that does not change during the game.
*/
config: BoardConfig;
/** Sections of the whole board, arranged in `sections[y][x]` fashion. */
sections: SectionData[][];
}

15
types/change.ts Normal file
View file

@ -0,0 +1,15 @@
import { CharacterPosition } from "./board.ts";
/**
* A change to the board.
*
* At least one field must be specified. The rules usually only allow one field at a time.
*/
export interface BoardChange extends CharacterPosition {
/** New character for this position. */
ch?: string;
/** New foreground color for this position. */
color?: string;
/** New background color for this position. */
bg_color?: string;
}

38
types/section.ts Normal file
View file

@ -0,0 +1,38 @@
/**
* A section of the whole board.
*
* This form is how board data actually stored in the backend. It's a more structured form than `FullBoard` designed for the ease and efficiency of manipulation.
*
* This structure should not be exposed to clients and renderers.
*/
export interface SectionData {
/** X axis position of this section's origin point (`(0, 0)`), in board's frame of reference. */
offsetX: number;
/** Y axis position of this section's origin point (`(0, 0)`), in board's frame of reference. */
offsetY: number;
/**
* Every character in this section.
*
* The first index is the Y axis, and the second index is the X axis, which means `ch[y][x]` points to the character at `(x, y)` in this section.
*
* Please pay special attention to that though we use the term *character*, each `ch[y][x]` is actually an [**extended grapheme cluster**](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (*user-perceived chracter*), not a single byte or character.
*/
ch: string[][];
/** Foreground color of each character in this section. */
color: string[][];
/** Background color of each character in this section. */
bgColor: string[][];
/**
* Width indicator of each character in this section.
*
* For every character, the width indicator is the number of display characters it occupies. For example, a full-width character occupies 2 display characters, while a half-width character occupies 1 display character.
*
* Every one of these numbers must be a integer between `1` and `9` (including both ends), as in compact string form it's encoded as a single digit in decimal.
*/
width: number[][];
}
export interface SectionPosition {
sx: number;
sy: number;
}