impl preprocessor

This commit is contained in:
monoid 2022-06-11 15:10:10 +09:00
parent c596a80518
commit ca9658be6b
16 changed files with 750 additions and 187 deletions

2
.gitignore vendored
View File

@ -2,5 +2,3 @@ book
.env
cache
.DS_Store
build

View File

@ -2,13 +2,15 @@
authors = ["monoid"]
language = "ko"
multilingual = false
src = "build"
src = "src"
title = "Software Requirement Specification"
[preprocessor]
[preprocessor.mermaid]
command = "mdbook-mermaid"
[preprocessor.etap]
command = "deno run -A --no-check tools/preprop.ts"
before = ["mermaid"]
[output]

21
cli.py
View File

@ -18,15 +18,6 @@ def updateIssue(issuePath: str, verbose = False):
cmd = ["deno", "run","--no-check", "-A","tools/getIssue.ts", "--path",issuePath]
cmdExecute(cmd, verbose, "update issue:")
def printDocument(issuePath:str, outDir:str, watch = False, verbose = False):
if verbose:
print("build document : issuePath(", issuePath, ") to ", outDir)
cmd = ["deno", "run","--no-check" ,"-A","tools/printDocument.ts", "--issue_path", issuePath, "--outDir", outDir]
if watch:
cmd.append("--watch")
p = subprocess.run(cmd)
p.check_returncode()
def build(args):
parser = argparse.ArgumentParser(description='Compiling the documentation', prog="cli.py build")
parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode')
@ -38,10 +29,10 @@ def build(args):
if args.verbose:
print("build start")
issuePath = os.path.join(args.outDir,"issues.json")
if args.update_issues:
os.makedirs("cache", exist_ok=True)
issuePath = os.path.join("cache","issues.json")
updateIssue(issuePath, args.verbose)
printDocument(issuePath, args.outDir, args.watch, args.verbose)
cmd = ["mdbook", "build"]
cmdExecute(cmd, args.verbose)
@ -57,11 +48,7 @@ def serve(args):
if args.verbose:
print("serve start")
cmd = ["mdbook", "serve", "--port", str(args.port)]
p = subprocess.Popen(cmd)
printDocument(issuePath, outDir, True, args.verbose)
p.wait()
if p.returncode != 0:
sys.exit(p.returncode)
cmdExecute(cmd, args.verbose, "serve:")
def help(_args):
global commandList
@ -80,7 +67,7 @@ def issueUpdate(args):
def buildPdf(args):
parser = argparse.ArgumentParser(description='Print to pdf', prog="cli.py buildPdf")
parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode')
parser.add_argument('--outDir', default="build/doc.pdf", help='output directory')
parser.add_argument('--outDir', default="cache/doc.pdf", help='output directory')
parser.add_argument('--browser-path', help='path to the browser')
args = parser.parse_args(args)
if os.path.exists(args.outDir):

1
log.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,4 +4,5 @@
- [Overall Description](./overall.md)
- [Specific Requirement](./specific.md)
- [Supporting information](./support.md)
- [Architecture](./architecture.md)
- [Architecture](./architecture.md)
- [Testing](./testing.md)

335
src/testing.md Normal file
View File

@ -0,0 +1,335 @@
# Testing
## 유닛 테스트
유닛 테스트로 69.6%의 Line Coverage와 73.4%의 Function Coverage를 달성했다.
다음과 같은 로그가 있다.
```
running 2 tests from ./src/auth/permission.test.ts
permission.test ... ok (8ms)
permission empty ... ok (16ms)
running 4 tests from ./src/auth/session.test.ts
Session ...
set ... ok (9ms)
delete ... ok (16ms)
ok (42ms)
Login Handler ...
login with invalid format ... ok (15ms)
login with invalid password ... ok (16ms)
login ... ok (16ms)
logout with no session ... ok (16ms)
logout ... ok (16ms)
ok (96ms)
getSession ... ok (16ms)
getSession with invalid cookie ... ok (16ms)
running 1 test from ./src/auth/user.test.ts
user.createAdminUser ... ok (15ms)
running 4 tests from ./src/document/filedoc.test.ts
readDocFile ... ok (19ms)
readDocFile: not found ... ok (16ms)
readDocFile: invalid json ... ok (16ms)
saveDocFile ... ok (15ms)
running 3 tests from ./src/router/methodHandle.test.ts
methodHandle: basic methods ... ok (8ms)
methodHandle: not found ... ok (16ms)
methodHandle: options ... ok (16ms)
running 8 tests from ./src/router/route.test.ts
route: basic route ... ok (10ms)
route: double slash route ... ok (16ms)
route: double match ... ok (16ms)
route: test context ... ok (16ms)
route: test regex ... ok (16ms)
route: test not found ... ok (16ms)
route: encode_route ... ok (2ms)
route: router in router ... ok (13ms)
running 4 tests from ./src/rpc/chunk.test.ts
basic chunk operation ...
create chunk ... ok (19ms)
delete chunk ... ok (15ms)
modify chunk ... ok (15ms)
move chunk ... ok (15ms)
invalid chunk operation ... ok (17ms)
ok (98ms)
test chunk notification operation ... ok (15ms)
test chunk conflict ... ok (16ms)
test chunk conflict resolve with history ... ok (32ms)
running 2 tests from ./src/rpc/doc.test.ts
handleDocumentMethod ... ok (4ms)
handleTagMethod ...
setTag ... ok (13ms)
getTag ... ok (15ms)
conflict ... ok (15ms)
ok (61ms)
running 3 tests from ./src/rpc/share.test.ts
handleShareGetInfo ... ok (18ms)
handleShareDocMethod ... ok (15ms)
handleShareMethod with no existing share token ... ok (16ms)
running 1 test from ./src/server.test.ts
server rpc test ... ok (1s)
running 3 tests from ./src/setting.test.ts
setting: basic ... ok (35ms)
setting: default value ... ok (7ms)
setting: defered register ... ok (16ms)
test result: ok. 35 passed (15 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (2s)
```
## 기능 테스트
### Chunk
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Focus/Unfocus</td>
<td>1. 청크를 클릭한다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>2</td>
<td>remove</td>
<td>1. 청크를 삭제하는 버튼을 클릭한다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>3-1</td>
<td>render - markdown</td>
<td>1. 마크다운 청크 렌더링을 확인한다.</td>
<td> # 제목 </td>
<td>P</td>
</tr>
<tr>
<td>3-2</td>
<td>render - latex</td>
<td>1. LaTex 청크 렌더링을 확인한다.</td>
<td> sum^n_{n=0}n = \frac{n(n+1)}2$$ </td>
<td>P</td>
</tr>
<tr>
<td>3-3</td>
<td>render - link</td>
<td>1. Image 청크 렌더링을 확인한다.</td>
<td>http://picsum.photos</td>
<td>P</td>
</tr>
<tr>
<td>4</td>
<td>previews</td>
<td>1. Katex 청크의 미리보기를 본다.</td>
<td> sum^n_{n=0}n = \frac{n(n+1)}2$$ </td>
<td>F</td>
</tr>
<tr>
<td>10</td>
<td>autocomplete</td>
<td>1. <kbd>Ctrl+Space</kbd>를 눌러 자동완성을 시도한다.</td>
<td></td>
<td>F</td>
</tr>
<tr>
<td>11</td>
<td>swap positions</td>
<td>1. 청크의 위치를 바꾼다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>27-1</td>
<td>edit</td>
<td>1. 청크를 수정한다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>27-2</td>
<td>edit chunk conflict</td>
<td>1. 청크를 수정모드에 들어간다.</td>
<td></td>
<td>F</td>
</tr>
</tbody>
</table>
### Document
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>view Chunk</td>
<td>1. 문서를 열어 청크가 렌더링되는지 본다.</td>
<td>test.syd</td>
<td>P</td>
</tr>
<tr>
<td>7</td>
<td>add/delete tag</td>
<td>1. 문서에 태그를 추가한다.<br>2. 문서에 태그를 삭제한다.</td>
<td>A</td>
<td>P</td>
</tr>
<tr>
<td>8</td>
<td>Drag And Drop Upload,</td>
<td>1. 텍스트를 드래그한다.</td>
<td></td>
<td>P</td>
</tr>
</tbody>
</table>
### File
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>14</td>
<td>create/delete/rename file</td>
<td>1. 파일을 만든다.</td>
<td>test.txt</td>
<td>P</td>
</tr>
<tr>
<td>15</td>
<td>upload/download files</td>
<td>1. 파일을 업로드한다.</td>
<td>test.txt</td>
<td>P</td>
</tr>
<tr>
<td>18</td>
<td>export document</td>
<td>1. export 버튼을 누른다.</td>
<td></td>
<td>F</td>
</tr>
</tbody>
</table>
### Search
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>16</td>
<td>Document Search</td>
<td>1. 검색버튼을 눌러 검색을 한다.</td>
<td>chunk</td>
<td>F</td>
</tr>
</tbody>
</table>
### Stash
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>17</td>
<td>render</td>
<td>1. 스태시가 그려지는지 확인한다</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>19</td>
<td>add</td>
<td>1. 청크를 추가한다</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>20</td>
<td>remove</td>
<td>1. 청크를 삭제한다</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>21</td>
<td>Drag and Drop to Document</td>
<td>1. 청크로부터 문서로 청크를 옮긴다.</td>
<td></td>
<td>P</td>
</tr>
</tbody>
</table>
### Management
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>22</td>
<td>Login</td>
<td>1. 비밀번호를 입력한다.</td>
<td>admin</td>
<td>F</td>
</tr>
<tr>
<td>24</td>
<td>Localization</td>
<td>1. 다른언어를 지원하는지 언어를 바꿔 확인한다</td>
<td></td>
<td>F</td>
</tr>
</tbody>
</table>

86
tools/preprop.ts Normal file
View File

@ -0,0 +1,86 @@
import { readAll } from "https://deno.land/std@0.143.0/streams/mod.ts";
import * as log from "https://deno.land/std@0.143.0/log/mod.ts";
import { WriterHandler } from "https://deno.land/std@0.143.0/log/handlers.ts";
import * as Eta from "https://deno.land/x/eta@v1.12.3/mod.ts";
import { Issue } from "./githubType.ts";
class StderrHandler extends WriterHandler {
protected _writer: Deno.Writer;
#encoder: TextEncoder;
constructor(levelName: log.LevelName, options: log.HandlerOptions = {}){
super(levelName, options);
this.#encoder = new TextEncoder();
this._writer = Deno.stderr;
}
log(msg: string): void {
const encoded = this.#encoder.encode(msg);
Deno.stderr.writeSync(encoded);
}
}
interface Book {
sections: Section[];
}
interface Section {
//chapter or separtor or PartTitle
Chapter: Chapter;
}
interface Chapter {
name: string;
content: string;
/** section number */
number?: number[];
sub_items: Section[];
path?: string;
source_path?: string;
parent_names: string[];
}
if (import.meta.main) {
await log.setup({
handlers: {
console: new StderrHandler("INFO", {
formatter: "{levelName} {msg}",
}),
},
loggers: {
default: {
level: "INFO",
handlers: ["console"],
}
}
}
);
main(Deno.args);
}
async function getIssues(){
const issue_path = "cache/issues.json";
const c = await Deno.readTextFile(issue_path);
const issues = JSON.parse(c) as Issue[];
issues.sort((a, b) => a.number - b.number);
return issues;
}
async function main(args: string[]) {
if (args.length > 1) {
//log.info(`args: ${JSON.stringify(args)}`);
if (args[0] === "supports") {
Deno.exit(0);
}
}
const issues = await getIssues();
log.info(`start`);
const data = await readAll(Deno.stdin);
const jsonText = new TextDecoder().decode(data);
await Deno.writeTextFile("log.json", jsonText);
const [context, book] = JSON.parse(jsonText) as [any, Book];
book.sections.forEach(x=>{
x.Chapter.content = Eta.render(x.Chapter.content, {
issues: issues
}) as string;
})
//Deno.stderr.writeSync(new TextEncoder().encode(`context: ${JSON.stringify(context)}\n`));
console.log(JSON.stringify(book));
}

View File

@ -1,137 +0,0 @@
import { Issue } from "./githubType.ts";
import { copy } from "https://deno.land/std@0.136.0/fs/mod.ts";
import { readAll } from "https://deno.land/std@0.135.0/streams/mod.ts"
import { parse as argParse } from "https://deno.land/std@0.135.0/flags/mod.ts";
import {
normalize, join as pathJoin, fromFileUrl, parse as parsePath
, relative
} from "https://deno.land/std@0.135.0/path/mod.ts";
import * as Eta from "https://deno.land/x/eta@v1.12.3/mod.ts";
import { createReactive } from "./reactivity.ts";
async function readContent(path?: string): Promise<string> {
let content = "[]";
if (path) {
content = await Deno.readTextFile(path);
}
else if (!Deno.isatty(Deno.stdin.rid)) {
const decoder = new TextDecoder(undefined, { ignoreBOM: true });
const buf = await readAll(Deno.stdin);
content = decoder.decode(buf);
}
else throw new Error("No input provided. path or stdin.");
return content;
}
type printDocParam = {
target: string,
data: {
issues: Issue[]
}
};
async function printDoc(param: printDocParam, option?: {
outDir?: string
}) {
option = option ?? {};
const { target, data } = param;
const { outDir } = option;
let print: string = "";
print = await Eta.renderFile(target, data) as string;
if (outDir) {
const outPath = pathJoin(outDir, target);
await Deno.mkdir(pathJoin(outDir), { recursive: true });
await Deno.writeTextFile(outPath, print);
}
else {
console.log(print);
}
}
async function main() {
const parsedArg = argParse(Deno.args);
const { issue_path, outDirArg, w, watch } = parsedArg;
const watchMode = w || watch;
if (typeof issue_path !== "undefined" && typeof issue_path !== "string") {
console.log("Please provide a path to the json file.");
Deno.exit(1);
}
if (typeof outDirArg !== "undefined" && typeof outDirArg !== "string") {
console.log("Please provide a path to the output file.");
Deno.exit(1);
}
const outDir = (outDirArg ?? "build");
if (typeof watchMode !== "undefined" && typeof watchMode !== "boolean") {
console.log("Please provide a boolean value for w.");
Deno.exit(1);
}
if (watchMode && typeof issue_path === "undefined") {
console.log("Could not set watch mode without a path.");
Deno.exit(1);
}
const url = new URL(import.meta.url)
url.pathname = normalize(pathJoin(url.pathname, "..", "template"));
const viewPath = fromFileUrl(url);
Eta.configure({
views: viewPath,
"view cache": false,
});
const issuesR = await createReactive(async () => {
const c = await readContent(issue_path);
const issues = JSON.parse(c) as Issue[];
issues.sort((a, b) => a.number - b.number);
return issues;
});
const targets = ["SUMMARY.md", "overall.md", "specific.md", "intro.md", "support.md", "architecture.md"];
const targetsR = await Promise.all(targets.map(async (t) => {
return await createReactive(async () => {
await printDoc({
target: t, data: {
issues: issuesR.value
}
}, { outDir: outDir });
});
}
));
issuesR.wireTo(...targetsR);
const copyOp = await createReactive(async () => {
const files = [...Deno.readDirSync(viewPath)].map(x => x.name).filter(x => !x.endsWith(".md"));
const op = files.map(x => copy(pathJoin(viewPath, x), pathJoin(outDir, x), { overwrite: true }));
await Promise.all(op);
});
if (watchMode) {
const watcher = Deno.watchFs([viewPath, issue_path as string]);
for await (const event of watcher) {
if (event.kind === "modify") {
Deno.stdout.write(
new TextEncoder().encode("\x1b[2J\x1b[0f"),
);
console.log(`reloading ${event.paths.join(", ")}`);
for (const path of event.paths) {
const p = parsePath(path);
if (p.dir === viewPath) {
if (p.ext === ".md") {
targetsR[targets.indexOf(p.base)].update();
}
else {
copyOp.update();
}
}
else if (p.base === "issues.json") {
await issuesR.update();
}
}
}
}
}
}
if (import.meta.main) {
main();
}

318
tools/printIssue.ts Normal file
View File

@ -0,0 +1,318 @@
type Testcase = {
id: number,
subId: number | null,
content: string,
procedure: string,
testData: string| null,
expected: string,
actual: string,
pass: boolean
}
const testcase: Testcase[] = [
{
"id": 1,
"subId": null,
"content": "Focus/Unfocus",
"procedure": "1. 청크를 클릭한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 2,
"subId": null,
"content": "remove",
"procedure": "1. 청크를 삭제하는 버튼을 클릭한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 1,
"content": "render - markdown",
"procedure": "1. 마크다운 청크 렌더링을 확인한다.",
"testData": " # 제목 ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 2,
"content": "render - latex",
"procedure": "1. LaTex 청크 렌더링을 확인한다.",
"testData": " sum^n_{n=0}n = \\frac{n(n+1)}2$$ ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 3,
"content": "render - link",
"procedure": "1. Image 청크 렌더링을 확인한다.",
"testData": " http://picsum.photos/200/300 ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 4,
"subId": null,
"content": "previews",
"procedure": "1. Katex 청크의 미리보기를 본다.",
"testData": " sum^n_{n=0}n = \\frac{n(n+1)}2$$ ",
"expected": "",
"actual": "",
"pass": false
},
{
"id": 10,
"subId": null,
"content": "autocomplete",
"procedure": "1. 자동완성을 시험한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 11,
"subId": null,
"content": "swap positions",
"procedure": "1. 청크의 위치를 바꾼다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 27,
"subId": 1,
"content": "edit",
"procedure": "1. 청크를 수정한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 27,
"subId": 2,
"content": "edit chunk conflict",
"procedure": "1. 청크를 수정모드에 들어간다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 5,
"subId": null,
"content": "view Chunk",
"procedure": "1. 문서를 열어 청크가 렌더링되는지 본다.",
"testData": "test.syd",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 7,
"subId": null,
"content": "add/delete tag",
"procedure": "1. 문서에 태그를 추가한다.<br>2. 문서에 태그를 삭제한다.",
"testData": "A",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 8,
"subId": null,
"content": "Drag And Drop Upload,",
"procedure": "1. 텍스트를 드래그한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 14,
"subId": null,
"content": "create/delete/rename file",
"procedure": "1. 파일을 만든다.",
"testData": "test.txt",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 15,
"subId": null,
"content": "upload/download files",
"procedure": "1. 파일을 업로드한다.",
"testData": "test.txt",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 18,
"subId": null,
"content": "export document",
"procedure": "1. export 버튼을 누른다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 17,
"subId": null,
"content": "render",
"procedure": "1. 스태시가 그려지는지 확인한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 19,
"subId": null,
"content": "add",
"procedure": "1. 청크를 추가한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 20,
"subId": null,
"content": "remove",
"procedure": "1. 청크를 삭제한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 21,
"subId": null,
"content": "Drag and Drop to Document",
"procedure": "1. 청크로부터 문서로 청크를 옮긴다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 22,
"subId": null,
"content": "Login",
"procedure": "1. 비밀번호를 입력한다.",
"testData": "admin",
"expected": "",
"actual": "",
"pass": false
},
{
"id": 24,
"subId": null,
"content": "Localization",
"procedure": "1. 다른언어를 지원하는지 언어를 바꿔 확인한다",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 16,
"subId": null,
"content": "Document Search",
"procedure": "1. 검색해본다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 85,
"subId": null,
"content": "Permission",
"procedure": "1. 각각의 기능들에 권한없이 시도한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
}
]
import {Issue} from "./githubType.ts"
async function readContent(path?: string): Promise<string> {
let content = "[]";
if (path) {
content = await Deno.readTextFile(path);
}
else throw new Error("No input provided. path or stdin.");
return content;
}
const data = await readContent("../build/issues.json")
const issues = JSON.parse(data) as Issue[]
const table = new Map<string, Issue[]>();
issues.forEach((x)=>{
const category = x.title.split(":")[0];
if(!category) return;
let c = table.get(category)
if(!c){
c = [];
table.set(category,c);
}
c.push(x);
})
const keys = Array.from(table.keys());
keys.forEach(x=>{
console.log(`\n### ${x}\n`);
const issues = table.get(x);
console.log("<table>");
console.log("<thead>");
console.log("<tr>");
//console.log("<th>Category</th>");
console.log("<th>ID</th>");
console.log("<th>Content</th>");
console.log("<th>Procedure</th>");
console.log("<th>Test Data</th>");
console.log("<th>P/F</th>");
console.log("</tr>");
console.log("</thead>");
console.log("<tbody>");
const ts = issues!.map(x=> testcase.filter(y=>y.id==x.number)).flat() as Testcase[];
if(ts?.length == 0) return;
//console.log(`<tr><th rowspan="${ts?.length}">${x}</th>`);
ts.forEach((y,i)=>{
//if(i>0)
console.log("<tr>");
const id = y.subId ? `${y.id}-${y.subId}` : y.id;
console.log(`<td>${id}</td>`);
console.log(`<td>${y.content}</td>`);
console.log(`<td>${y.procedure}</td>`);
console.log(`<td>${y.testData ?? ""}</td>`);
console.log(`<td>${y.pass ? "P" : "F"}</td>`);
console.log("</tr>");
})
console.log("</tbody>");
console.log("</table>");
})

View File

@ -1,28 +0,0 @@
interface Reactive<T>{
readonly value: T;
update: () => Promise<void>;
wireTo(...r: Reactive<unknown>[]): void;
unwireTo(r: Reactive<unknown>): void;
}
export async function createReactive<T>(fn: () => Promise<T>): Promise<Reactive<T>> {
let v = await fn();
let listeners: Reactive<unknown>[] = [];
return {
get value() : T {
return v;
},
async update(){
const ret = await fn();
v = ret;
await Promise.all(listeners.map(o => o.update()));
},
wireTo(...r: Reactive<unknown>[]) {
listeners.push(...r);
},
unwireTo(r: Reactive<unknown>) {
listeners = listeners.filter(o => o !== r);
}
};
}