Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Name Checker #8

Merged
merged 14 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
},
"dependencies": {
"ajv": "^8.12.0",
"comlink": "^4.4.1",
"dt-python-parser": "^0.9.0",
"json-source-map": "^0.6.1",
"octokit": "^3.1.2"
}
Expand Down
1 change: 1 addition & 0 deletions src/components/BetaTag.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span class="rounded-full bg-blue-500 px-1.5 align-super text-base text-white">beta</span>
12 changes: 10 additions & 2 deletions src/components/Message.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<script lang="ts">
import { Icon, InformationCircle, ExclamationTriangle, ExclamationCircle, CheckCircle } from "svelte-hero-icons";
import {
Icon,
InformationCircle,
ExclamationTriangle,
ExclamationCircle,
CheckCircle,
QuestionMarkCircle
} from "svelte-hero-icons";

export let title: string = "";
export let text: string = "";
Expand All @@ -9,7 +16,8 @@
info: ["bg-info-bg", "text-info-text", InformationCircle],
warning: ["bg-warning-bg", "text-warning-text", ExclamationTriangle, "translate-y-[2px]"],
error: ["bg-error-bg", "text-error-text", ExclamationCircle],
good: ["bg-good-bg", "text-good-text", CheckCircle]
good: ["bg-good-bg", "text-good-text", CheckCircle],
neutral: ["bg-neutral-400", "text-white", QuestionMarkCircle]
};
</script>

Expand Down
13 changes: 13 additions & 0 deletions src/components/NameInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
export let placeholder: string = "Enter your name";
export let maxlength: number = 20;
</script>

<input
on:input
type="text"
{placeholder}
{maxlength}
autocapitalize="words"
class="rounded-md border-2 border-neutral-300 p-3 text-2xl italic"
/>
62 changes: 62 additions & 0 deletions src/lib/curses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Octokit } from "octokit";
import { tokenize, type SimpleToken } from "./python";

const octokit = new Octokit();
const repoParam = {
owner: "Monika-After-Story",
repo: "MonikaModDev",
ref: "master"
};

const regexpLookupPath = "Monika After Story/game/script-story-events.rpy";

export async function getNameRegexps() {
const res = await octokit.rest.repos.getContent({
...repoParam,
path: regexpLookupPath,
mediaType: { format: "raw" }
});

const script = res.data.toString();
const tokens = await tokenize(script);

const badNicknames = getListStringContents(tokens, "mas_bad_nickname_list");
const goodNicknamesBase = getListStringContents(tokens, "mas_good_nickname_list_base");
const goodNicknamesPlayerMod = getListStringContents(tokens, "mas_good_nickname_list_player_modifiers");
const goodNicknamesMonikaMod = getListStringContents(tokens, "mas_good_nickname_list_monika_modifiers");
const awkwardNicknames = getListStringContents(tokens, "mas_awkward_nickname_list");

const toRegExp = (it: string) => new RegExp(it, "i");
const bad = [...badNicknames.map(toRegExp), /badname/i];
const playerGood = [...goodNicknamesBase, ...goodNicknamesPlayerMod].map(toRegExp);
const monikaGood = [...goodNicknamesBase, ...goodNicknamesMonikaMod].map(toRegExp);
const awkward = awkwardNicknames.map(toRegExp);
return { bad, awkward, playerGood, monikaGood };
}

function getListStringContents(tokens: SimpleToken[], varName: string): string[] {
const defIdx = tokens.findIndex((it) => it.type === 37 && it.token === varName);
if (defIdx < 0) return [];
const openIdx = tokens.slice(defIdx).findIndex((it) => it.type === 56);
if (openIdx < 0) return [];
const closeIdx = tokens.slice(defIdx).findIndex((it) => it.type === 57);
if (closeIdx < 0) return [];
// @prettier-ignore
return tokens
.slice(defIdx + openIdx, defIdx + openIdx + closeIdx)
.filter((it) => it.type === 38)
.map((it) =>
"rb".includes(it.token.charAt(0))
? it.token.substring(2, it.token.length - 1)
: it.token.substring(1, it.token.length - 1)
);
}

export function checkName(name: string, regExps: Record<string, RegExp[]>): [string, RegExp] | null {
// @prettier-ignore
const found = Object.entries(regExps)
.map(([key, re]) => [key, re.find((it) => it.test(name))])
.filter((it) => it[1]);
if (found.length === 0) return null;
return found[0] as [string, RegExp];
}
15 changes: 15 additions & 0 deletions src/lib/python.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type Remote } from "comlink";

import WebPythonWorker from "./python.worker?worker";
import PythonWorker from "./python.worker";
import { runWorkerCtx } from "./worker";

export type AntlrToken = { type: number; start: number; stop: number };
export type SimpleToken = { type: number; token: string };

export async function tokenize(source: string): Promise<SimpleToken[]> {
return runWorkerCtx(WebPythonWorker, async (worker: Remote<unknown>): Promise<SimpleToken[]> => {
const tokens = await (worker as Remote<PythonWorker>).tokenize(source);
return tokens.map((it) => ({ type: it.type, token: source.substring(it.start, it.stop + 1) }));
});
}
16 changes: 16 additions & 0 deletions src/lib/python.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Comlink from "comlink";

// @ts-expect-error because package is built with untyped JS
import { Python3Parser } from "dt-python-parser";
import { type AntlrToken } from "./python";

export default class PythonWorker {
public tokenize(source: string): AntlrToken[] {
const parser = new Python3Parser();
const tokens = parser.getAllTokens(source);
// Need to simplify tokens to exclude circular dependencies and allow serialization
return tokens.map((it: AntlrToken) => ({ type: it.type, start: it.start, stop: it.stop }));
}
}

Comlink.expose(new PythonWorker());
17 changes: 17 additions & 0 deletions src/lib/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as Comlink from "comlink";

type WorkerCallback<W extends Worker, T> = (worker: Comlink.Remote<W>) => Promise<T>;
type WorkerConstructor<T extends Worker> = { new (): T };

export async function runWorkerCtx<W extends Worker, T>(
workerConstructor: WorkerConstructor<W>,
callback: WorkerCallback<W, T>
): Promise<T> {
const proxy = Comlink.wrap(new workerConstructor()) as Comlink.Remote<W>;

try {
return await callback(proxy);
} finally {
proxy[Comlink.releaseProxy]();
}
}
1 change: 1 addition & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<div class="flex flex-col justify-center">
<ul>
<li class="font-medium text-blue-500"><a href="/validator">🔧 JSON Validator</a></li>
<li class="font-medium text-blue-500"><a href="/curses">🤬 Name Checker</a></li>
</ul>
</div>
<div class="flex h-1/3 flex-col justify-end">
Expand Down
116 changes: 116 additions & 0 deletions src/routes/curses/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<script lang="ts">
// Main tool layout
import ToolLayout from "../../layouts/ToolLayout.svelte";
import SideLink from "../../components/SideLink.svelte";
import SideParagraph from "../../components/SideParagraph.svelte";

import NameInput from "../../components/NameInput.svelte";
import Message from "../../components/Message.svelte";
import BetaTag from "../../components/BetaTag.svelte";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let libCurses: any;
import { onMount } from "svelte";

let regexps: Record<string, RegExp[]>;
let loadingPromise: Promise<Record<string, RegExp[]>>;

let name: string;
let matchKey: string;
let matchRule: RegExp;

onMount(async () => {
libCurses = await import("$lib/curses");
loadingPromise = libCurses.getNameRegexps();
regexps = await loadingPromise;
});

function onNameInput(e: Event) {
name = (e.target as HTMLInputElement).value;
const match = libCurses.checkName(name, regexps);
[matchKey, matchRule] = match ?? [null, null];
}
</script>

<ToolLayout>
<svelte:fragment slot="title">
MAS Name Checker <BetaTag />
</svelte:fragment>
<svelte:fragment slot="subtitle">Check if a name is good, bad, awkward or okay with Monika</svelte:fragment>
<svelte:fragment slot="tool">
{#await loadingPromise}
<div class="flex justify-center">
<div class="w-full lg:w-2/3">
<Message title="Loading" text="Please wait while the tool loads..." type="neutral" />
</div>
</div>
{:then}
<div class="flex justify-center">
<div class="w-full lg:w-2/3">
{#if matchKey === undefined || name?.length === 0}
<Message title="Enter your name" text="To check a name, type it in the box below." type="neutral" />
{:else if !matchKey}
<Message
title="This name is okay"
text="Not bad, but nothing too special either. A good choice."
type="neutral"
/>
{:else if matchKey === "playerGood"}
<Message
title="This name is good!"
text="If you name yourself like that, Monika will like it."
type="good"
/>
{:else if matchKey === "monikaGood"}
<Message
title="This name is good!"
text="If you give your Monika this name, she will like it."
type="good"
/>
{:else if matchKey === "awkward"}
<Message
title="This name is... awkward."
text="If you try to name yourself or your Monika like that, she will not like it."
type="warning"
/>
{:else if matchKey === "bad"}
<Message
title="This name is terrible!"
text="If you try to name yourself or your Monika like that, she will be furious!"
type="error"
/>
{/if}
</div>
</div>
<div class="flex flex-col place-items-center gap-2">
<div class="mb-3">
<NameInput on:input={onNameInput} />
</div>
{#if matchRule}
<p class="text-center">
Matched rule: <span class="rounded-md bg-neutral-200 px-1 py-0.5 font-mono">{matchRule.toString()}</span>
</p>
{/if}
</div>
{/await}
</svelte:fragment>
<svelte:fragment slot="links">
<SideLink
href="https://github.com/Monika-After-Story/MonikaModDev/blob/06baf319a34c2ef585bc7c0a1e969a7eaa894b35/Monika%20After%20Story/game/script-story-events.rpy#L222-L476"
>
📜 MAS Source code to classify names
</SideLink>
<SideLink href="https://github.com/Friends-of-Monika/mas-web-utils">🧪 MAS Web Utils Github repository</SideLink>
<SideLink href="https://github.com/Friends-of-Monika/mas-web-utils/issues/new">🐛 Report an issue</SideLink>
</svelte:fragment>
<svelte:fragment slot="about">
<SideParagraph>
This tool will help you preview how Monika will see a name you put in without having to try your Monika's
patience.
</SideParagraph>
<SideParagraph>
<b>This is public beta.</b> While this tool has been tested, it could still produce false positives/negatives &mdash;
if you're absolutely sure that the result is incorrect consider reporting an issue.
</SideParagraph>
</svelte:fragment>
</ToolLayout>
8 changes: 8 additions & 0 deletions src/routes/curses/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function load() {
return {
meta: {
title: "Name Checker",
description: "🤬 An online tool to check how Monika will find a name"
}
};
}
4 changes: 2 additions & 2 deletions src/routes/validator/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import UploadButton from "../../components/UploadButton.svelte";
import CodeArea from "../../components/CodeArea.svelte";
import Message from "../../components/Message.svelte";
import BetaTag from "../../components/BetaTag.svelte";

import { Icon, CodeBracketSquare } from "svelte-hero-icons";

Expand Down Expand Up @@ -111,8 +112,7 @@

<ToolLayout>
<svelte:fragment slot="title">
MAS Spritepack JSON validator
<span class="rounded-full bg-blue-500 px-1.5 align-super text-base text-white">beta</span>
MAS Spritepack JSON validator <BetaTag />
</svelte:fragment>
<svelte:fragment slot="subtitle">Pick a file and see if it'll work right away</svelte:fragment>
<svelte:fragment slot="tool">
Expand Down
33 changes: 33 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,13 @@ __metadata:
languageName: node
linkType: hard

"@types/antlr4@npm:4.7.2":
version: 4.7.2
resolution: "@types/antlr4@npm:4.7.2"
checksum: 10c0/458ef8fe4c0525db630e588221978151b6b827bc16dee5ff3ed4ed9f529d750afd1dd54453f87e80bfa8e39f9abb1b7207d38b678cd7151bf090d59765917912
languageName: node
linkType: hard

"@types/aws-lambda@npm:^8.10.83":
version: 8.10.136
resolution: "@types/aws-lambda@npm:8.10.136"
Expand Down Expand Up @@ -1139,6 +1146,13 @@ __metadata:
languageName: node
linkType: hard

"antlr4@npm:4.8.0":
version: 4.8.0
resolution: "antlr4@npm:4.8.0"
checksum: 10c0/f2b836f7439f05719bd12f9e124ec8e276aa5a270d53de26a10f48700b6ba9563204a66d8bfbb94f4fb44d7c4455780a49d57fb0c25257912f3d456b6e2d6208
languageName: node
linkType: hard

"any-promise@npm:^1.0.0":
version: 1.3.0
resolution: "any-promise@npm:1.3.0"
Expand Down Expand Up @@ -1417,6 +1431,13 @@ __metadata:
languageName: node
linkType: hard

"comlink@npm:^4.4.1":
version: 4.4.1
resolution: "comlink@npm:4.4.1"
checksum: 10c0/a7a2004030768d13ec9373f780aa0edae57616095cb453ec8950d9f97e5fa654c0e84030ac87173b818843a04a5fa9ce9749d48b6bd453cfbfb5e6a9ebb6b2ff
languageName: node
linkType: hard

"commander@npm:^4.0.0":
version: 4.1.1
resolution: "commander@npm:4.1.1"
Expand Down Expand Up @@ -1566,6 +1587,16 @@ __metadata:
languageName: node
linkType: hard

"dt-python-parser@npm:^0.9.0":
version: 0.9.0
resolution: "dt-python-parser@npm:0.9.0"
dependencies:
"@types/antlr4": "npm:4.7.2"
antlr4: "npm:4.8.0"
checksum: 10c0/e19fa72c30346dfdd0b979c12333f3e1bce62bc81116d3936f4c1e1a2a71676d1e306d923b95a997c80c1c47219409af6fbad25555d366f1f7ef5cfcd61b710f
languageName: node
linkType: hard

"eastasianwidth@npm:^0.2.0":
version: 0.2.0
resolution: "eastasianwidth@npm:0.2.0"
Expand Down Expand Up @@ -2701,6 +2732,8 @@ __metadata:
"@typescript-eslint/parser": "npm:^7.0.0"
ajv: "npm:^8.12.0"
autoprefixer: "npm:^10.4.18"
comlink: "npm:^4.4.1"
dt-python-parser: "npm:^0.9.0"
eslint: "npm:^8.56.0"
eslint-config-prettier: "npm:^9.1.0"
eslint-plugin-svelte: "npm:^2.35.1"
Expand Down