M build.zig => build.zig +1 -1
@@ 5,7 5,7 @@ pub fn build(b: *std.build.Builder) void {
const mode = b.standardReleaseOptions();
const target = zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-freestanding" }) catch unreachable;
- const lib = b.addSharedLibrary("kana", "src/kana.zig", .unversioned);
+ const lib = b.addSharedLibrary("kana", "site/kana.zig", .unversioned);
lib.setBuildMode(mode);
lib.setTarget(target);
lib.addPackagePath("kana", "deps/kana/src/lib.zig");
M deno.json => deno.json +4 -2
@@ 7,7 7,9 @@
},
"tasks": {
"build": "deno run --allow-read --allow-write --allow-run ./scripts/build.ts",
- "serve": "deno run --allow-read --allow-net=:8000 src/server.ts",
- "prepare": "deno task build && mkdir -p dist && cp -r www src/server.ts dist/"
+ "build-watch": "deno run --allow-read --allow-write --allow-run --watch=./site ./scripts/build.ts",
+ "serve": "deno run --allow-read --allow-net=:8000 main.ts",
+ "serve-watch": "deno run --allow-read --allow-net=:8000 --watch=/tmp/kana-guru-sync main.ts",
+ "watch": "deno task build-watch & deno task --cwd server serve-watch"
}
}
M scripts/build.ts => scripts/build.ts +9 -5
@@ 1,14 1,17 @@
import { Language, minify } from "https://deno.land/x/minifier@v1.1.1/mod.ts";
-await Deno.mkdir("www", { recursive: true });
+const encoder = new TextEncoder();
+const now = new Date();
+
+await Deno.mkdir("server/assets", { recursive: true });
async function minifyFile(lang: Language, src: string, dst?: string): Promise<void> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
- const bytes = await Deno.readFile(`src/site/${src}`);
+ const bytes = await Deno.readFile(`site/${src}`);
const minified = minify(lang, decoder.decode(bytes));
- return Deno.writeFile(`www/${dst || src}`, encoder.encode(minified));
+ return Deno.writeFile(`server/assets/${dst || src}`, encoder.encode(minified));
}
const tasks = [
@@ 16,11 19,12 @@ const tasks = [
minifyFile(Language.CSS, "style.css", "style.min.css"),
minifyFile(Language.JS, "kana.js", "kana.min.js"),
Deno.run({
- cmd: ["zig", "build", "-Drelease-fast", "--prefix", "www"],
+ cmd: ["zig", "build", "-Drelease-fast", "--prefix", "assets"],
env: {
- "DESTDIR": "",
+ "DESTDIR": "server",
},
}).status(),
];
await Promise.all(tasks);
+await Deno.writeFile("/tmp/kana-guru-sync", encoder.encode(now.toString()), { mode: 0o777 });
A server/assets/favicon.svg => server/assets/favicon.svg +36 -0
@@ 0,0 1,36 @@
+<?xml version="1.0" encoding="windows-1252"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<circle style="fill:#F0F0F0;" cx="256" cy="256" r="256"/>
+<circle style="fill:#D80027;" cx="256" cy="256" r="111.304"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg><
\ No newline at end of file
A server/assets/index.html => server/assets/index.html +1 -0
@@ 0,0 1,1 @@
+<!DOCTYPE html><html><head><title>kana.guru - Convert romaji to hiragana/katakana</title><meta charset=utf-8><meta name=viewport content="width=device-width, initial-scale=1"><link rel=icon href=favicon.svg><link rel=stylesheet href=style.css><script type=module src=kana.js></script><body><div><h2>かな.グル <span id=copyright> © <span id=copyright-year></span> Gabriel Sanches / <a href=https://git.sr.ht/~gbrlsnchs/kana-guru>source code</a> </span></h2><h3>Options</h3><div><input id=katakana type=checkbox><label for=katakana>Start with katakana</label></div><div><input id=extended type=checkbox><label for=extended>Extended katakana</label></div><div><input id=punctuation type=checkbox><label for=punctuation>Parse punctuation</label></div><div><input id=force-prolongation type=checkbox><label for=force-prolongation>Force prolongation</label></div><div><input id=kana-toggle type=checkbox><label for=kana-toggle>Use @ to toggle kanas</label></div><div><input id=raw-toggle type=checkbox><label for=raw-toggle>Use # to toggle raw text</label></div><div><input id=prolongation-reset type=checkbox><label for=prolongation-reset>Use ^ to reset prolongation</label></div><div><input id=vowel-shortener type=checkbox><label for=vowel-shortener>Use _ to shorten vowel</label></div><div><input id=virtual-stop type=checkbox><label for=virtual-stop>Use % to add virtual stop</label></div><h3>Input</h3><textarea id=romaji autocomplete=off spellcheck=false></textarea><div id=output><h3>Output</h3><button id=copy>(copy)</button></div><p id=result></div><
\ No newline at end of file
A server/assets/kana.min.js => server/assets/kana.min.js +1 -0
@@ 0,0 1,1 @@
+let result = ""; const inputs = { romaji: document.getElementById("romaji"), katakana: document.getElementById("katakana"), extended: document.getElementById("extended"), punctuation: document.getElementById("punctuation"), forceProlongation: document.getElementById("force-prolongation"), kanaToggle: document.getElementById("kana-toggle"), rawToggle: document.getElementById("raw-toggle"), prolongationReset: document.getElementById("prolongation-reset"), vowelShortener: document.getElementById("vowel-shortener"), virtualStop: document.getElementById("virtual-stop"), }; const output = document.getElementById("result"); const loading = document.getElementById("loading"); const copy = document.getElementById("copy"); const encodeString = (value) => { const buffer = new TextEncoder().encode(value); const ptr = allocString(buffer.length + 1); const slice = new Uint8Array(memory.buffer, ptr, buffer.length + 1); slice.set(buffer); slice[buffer.length] = 0; return ptr; }; const decodeString = (ptr, len) => { const slice = new Uint8Array(memory.buffer, ptr, len); return new TextDecoder().decode(slice); }; const parseCheckbox = (el) => { if (!el) { return false; } return el.checked; }; const parseAscii = (el, char) => { return parseCheckbox(el) ? char.charCodeAt(0) : 0; }; const convert = () => { setTimeout(() => {}, 0); const ptr = encodeString(inputs.romaji.value); transliterate( ptr, parseCheckbox(inputs.katakana), parseCheckbox(inputs.extended), parseCheckbox(inputs.punctuation), parseCheckbox(inputs.forceProlongation), parseAscii(inputs.kanaToggle, "@"), parseAscii(inputs.rawToggle, "#"), parseAscii(inputs.prolongationReset, "^"), parseAscii(inputs.vowelShortener, "_"), parseAscii(inputs.virtualStop, "%"), ); freeString(ptr); }; const initialYear = 2023; const currentYear = new Date().getUTCFullYear(); const copyright = document.getElementById("copyright-year"); copyright.innerText = initialYear === currentYear ? initialYear : `${initialYear}-${currentYear}`; const { instance: { exports: { memory, allocString, freeString, transliterate }, }, } = await WebAssembly.instantiateStreaming(fetch("kana.wasm"), { env: { handleResult(ptr, len) { output.innerText = len > 0 ? decodeString(ptr, len) : ""; }, }, }); for (let [key, value] of Object.entries(inputs)) { value.addEventListener(key === "romaji" ? "keyup" : "input", () => { convert(); }); } copy.addEventListener("click", () => { navigator.clipboard.writeText(output.innerText || output.textContent); });<
\ No newline at end of file
A server/assets/lib/kana.wasm => server/assets/lib/kana.wasm +0 -0
A server/assets/style.min.css => server/assets/style.min.css +1 -0
@@ 0,0 1,1 @@
+:root { --bg-color: #1d1f21; --fg-color: #c5c8c6; --gray: #373b41; --copy: #8c9440; --link: #5f819d; } @media (prefers-color-scheme: light) { :root { --bg-color: #c5c8c6; --fg-color: #1d1f21; --gray: #707880; --copy: #5e8d87; --link: #a54242; } } body { margin: 0; padding: 0; width: 100vw; display: flex; justify-content: center; background-color: var(--bg-color); color: var(--fg-color); overflow: auto; font-family: monospace; } body > div { display: flex; flex-direction: column; gap: 8px; width: 640px; padding: 16px; } @media (max-width: 640px) { body > div { width: 100vw; } } h2 { display: flex; align-items: center; justify-content: space-between; } @media (max-width: 640px) { h2 { flex-direction: column; gap: 8px; } } h2, h3 { overflow: hidden; position: relative; margin-bottom: 0; } p { margin: 0; } h3:first-of-type:after { content: ""; width: 100%; height: 1px; background: var(--gray); position: absolute; top: 50%; margin-left: 16px; } a { color: var(--gray); text-decoration: none; } a:hover { color: var(--link); text-decoration: underline; } input[type="checkbox"] { margin-right: 8px; } label:hover, input[type="checkbox"]:hover { cursor: pointer; } textarea { height: 128px; resize: none; overflow-y: scroll; font-size: inherit; border: 1px solid var(--gray); padding: 8px; } textarea, input[type="checkbox"] { background-color: var(--bg); color: var(--fg); } #copyright { color: var(--gray); font-size: 0.8rem; } #result { white-space: pre-line; overflow-wrap: anywhere; } #copy { font-family: monospace; font-size: 0.8rem; font-weight: bold; color: var(--fg-color); border: 0; background-color: var(--bg-color); padding: 0; } #copy:hover { cursor: pointer; color: var(--copy); } #output { display: flex; flex-direction: row; justify-content: space-between; align-items: end; gap: 16px; } #output > h3 { flex: 1; }<
\ No newline at end of file
A server/main.ts => server/main.ts +35 -0
@@ 0,0 1,35 @@
+import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
+import { Status } from "https://deno.land/std@0.177.0/http/http_status.ts";
+
+const [index, style, core, wasm, favicon] = await Promise.all([
+ Deno.readFile("assets/index.html"),
+ Deno.readFile("assets/style.min.css"),
+ Deno.readFile("assets/kana.min.js"),
+ Deno.readFile("assets/lib/kana.wasm"),
+ Deno.readFile("assets/favicon.svg"),
+]);
+
+const assets: Map<string, [Uint8Array, string]> = new Map([
+ ["/", [index, "text/html"]],
+ ["/style.css", [style, "text/css"]],
+ ["/kana.js", [core, "text/javascript"]],
+ ["/kana.wasm", [wasm, "application/wasm"]],
+ ["/favicon.svg", [favicon, "image/svg+xml"]],
+]);
+
+function handler(req: Request) {
+ const url = new URL(req.url);
+ const { pathname } = url;
+
+ const asset = assets.get(pathname);
+
+ if (!asset) {
+ return Response.redirect(url.origin);
+ }
+
+ const [data, mimeType] = asset;
+
+ return new Response(data, { headers: { "content-type": mimeType } });
+}
+
+serve(handler, { port: 8000 });
R src/site/index.html => site/index.html +5 -1
@@ 4,6 4,7 @@
<title>kana.guru - Convert romaji to hiragana/katakana</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="favicon.svg" />
<link rel="stylesheet" href="style.css" />
<script type="module" src="kana.js"></script>
</head>
@@ 66,7 67,10 @@
<h3>Input</h3>
<textarea id="romaji" autocomplete="off" spellcheck="false"></textarea>
- <h3>Output <span id="copy" tabindex="0">(copy)</span></h3>
+ <div id="output">
+ <h3>Output</h3>
+ <button id="copy">(copy)</button>
+ </div>
<p id="result"></p>
</div>
</body>
R src/site/kana.js => site/kana.js +0 -0
R src/kana.zig => site/kana.zig +0 -0
R src/site/style.css => site/style.css +25 -6
@@ 10,7 10,8 @@
:root {
--bg-color: #c5c8c6;
--fg-color: #1d1f21;
- --copy: #de935f;
+ --gray: #707880;
+ --copy: #5e8d87;
--link: #a54242;
}
}
@@ 65,15 66,14 @@ p {
margin: 0;
}
-h3:first-of-type:after,
-h3:last-of-type:after {
+h3:first-of-type:after {
content: "";
width: 100%;
height: 1px;
background: var(--gray);
position: absolute;
top: 50%;
- margin-left: 1rem;
+ margin-left: 16px;
}
a {
@@ 83,6 83,7 @@ a {
a:hover {
color: var(--link);
+ text-decoration: underline;
}
input[type="checkbox"] {
@@ 95,7 96,7 @@ input[type="checkbox"]:hover {
}
textarea {
- height: 8rem;
+ height: 128px;
resize: none;
overflow-y: scroll;
font-size: inherit;
@@ 116,14 117,32 @@ input[type="checkbox"] {
#result {
white-space: pre-line;
- overflow-wrap: break-word;
+ overflow-wrap: anywhere;
}
#copy {
+ font-family: monospace;
font-size: 0.8rem;
+ font-weight: bold;
+ color: var(--fg-color);
+ border: 0;
+ background-color: var(--bg-color);
+ padding: 0;
}
#copy:hover {
cursor: pointer;
color: var(--copy);
}
+
+#output {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: end;
+ gap: 16px;
+}
+
+#output > h3 {
+ flex: 1;
+}
D src/server.ts => src/server.ts +0 -52
@@ 1,52 0,0 @@
-import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
-import { Status } from "https://deno.land/std@0.177.0/http/http_status.ts";
-
-const [index, style, core, wasm] = await Promise.all([
- Deno.readFile("www/index.html"),
- Deno.readFile("www/style.min.css"),
- Deno.readFile("www/kana.min.js"),
- Deno.readFile("www/lib/kana.wasm"),
-]);
-
-function handler(req: Request) {
- const { pathname } = new URL(req.url);
-
- if (pathname === "/") {
- return new Response(index, {
- headers: {
- "content-type": "text/html",
- },
- });
- }
-
- if (pathname.startsWith("/style.css")) {
- return new Response(style, {
- headers: {
- "content-type": "text/css",
- },
- });
- }
-
- if (pathname.startsWith("/kana.js")) {
- return new Response(core, {
- headers: {
- "content-type": "text/javascript",
- },
- });
- }
-
- if (pathname.startsWith("/kana.wasm")) {
- return new Response(wasm, {
- headers: {
- "content-type": "application/wasm",
- },
- });
- }
-
- const url = new URL(req.url);
- return Response.redirect(url.origin);
-}
-
-serve(handler, {
- port: 8000,
-});