ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
히라가나·카타카나·수학 워크시트와 웹 학습을 무료로 제공, 매번 무작위 문제와 정답 페이지 제공, 회원가입 불필요
Free worksheet maker and web learning for hiragana, katakana and math: new randomly generated practice every time, with answer pages and no sign-up.
교육JavaScript · HTMLGitHub @ys-learning-lab온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Home schooling material
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Static, no-build homeschool worksheet site. Everything runs client-side:
HTML5 + Tailwind CSS (CDN) + vanilla JS + pdf-lib (CDN)
for PDF generation. No server, no build step, no analytics beyond whatever
ad script you add yourself.README 전문 보기 ↗Static, no-build homeschool worksheet site. Everything runs client-side: HTML5 + Tailwind CSS (CDN) + vanilla JS + pdf-lib (CDN) for PDF generation. No server, no build step, no analytics beyond whatever ad script you add yourself.
index.html - landing page: a Japanese section with the same cards as japanese.html (printables), then an online-learning section (Tap Practice, Word Puzzle, heading links to kana-online.html), then a Math section below it
Each section heading is a keycap link (btn-key inside the <h2>) to japanese.html or math.html; card titles are <h3>.japanese.html / js/i18n-ja-hub.js - the Japanese landing page the top bar's "Japanese" button opens on every page.
Big cards in learning order: Study Plan, Kana Chart, Writing Practice, Kana Matching, Conversion Quiz, then an "Online learning" section (heading links to kana-online.html) with Tap Practice and Word Puzzle
(card text reuses the idx.* strings in i18n-ja-index.js), then a short "Where to start". No ads, no sub-nav.
A new Japanese page needs a card here and on index.html, a sub-nav tab, and a sitemap entry (online tools: see kana-online.html).math.html / js/math-generator.js / js/math-ui.js - port of source/math test advanced.pykana-quiz.html / js/japanese-generator.js / js/japanese-ui.js - port of source/Hiragana Katakana Test Maker.py
(the katakana<->hiragana conversion quiz)kana-matching.html / js/kana-matching-generator.js / js/kana-matching-ui.js - draw-a-line matching worksheet
(distinct from the conversion quiz above, where the student writes the answer). One worksheet, two vertical
columns of N boxes each (N = "Number of choices", 2-20; box height shrinks via _rowGeo() so 20 rows fit one
page). The ticked rows' kana are always used; if N is larger the rest is random (_randomFill(), split by the
Regular kana % between basic and dakuten/handakuten), if smaller N are sampled from the rows; no rows = all random.
The right column is shuffled so no kana sits straight across from its partner. "Trick choices" add the same
number of partner-less kana (pair: null) to BOTH columns, in random positions and with different kana on each
side, and they count toward N, so column lengths never reveal them. Every worksheet prints the same instruction
(circle a box with no match and write it in the other form beside it) whether or not it has trick choices, and
every box gets a faint writing line, so the sheet gives nothing away; the answer key crosses the trick boxes and
writes the converted kana. Match types: hiragana-katakana, hiragana-romaji, katakana-romaji. The preview's
worksheet is cached and passed straight to the renderer, so the PDF is exactly what was shown (only a settings
change or "Draw again" builds a new one). No row names (行) are printed because the embedded font subset lacks 行.
ぢ/づ show as "di"/"du" in romaji so they stay distinct from じ/ず.
The worksheet page carries the same answer QR as the other tools (top right, print code XXXX0000-A-1 below it,
toggle #cfg-qr, on by default; skipped if the payload is too dense to scan at 64pt). The payload is
label:answer entries read by answers.html - each left box and its partner, trick boxes as none (X), then any
right-hand trick boxes - and the instruction text wraps narrower to leave room for the QR.kana-tap-practice.html / js/kana-tap-ui.js / js/i18n-ja-tap.js - an on-screen (not printable) tap quiz: no PDF, no
generator class. A teacher/parent sets Question types and Choice types (each a set of hiragana / katakana / romaji
checkboxes; every question picks a random (question, choice) pair of different ticked types), kana rows (reuses
MATCH_ROWS from kana-matching-generator.js, which this page therefore also loads), choices per question (2-8, default 6),
number of questions (1-100; each ticked kana once before any repeats), "wrong choices from the ticked kana"
(tops up from all kana), and immediate right/wrong feedback (a miss waits for Next, a hit advances itself).
Three screens (setup / play / results) render from one state object, so a language switch re-renders mid-quiz.
Results show score, % and a table of every question (asked, picked, correct) with a "show only missed" filter, plus
Play again / Practice missed only / Change settings. Each question is timed from when it appears to the tap; results show
per-question times, the total (sum of those) and the average. The setup (not scores) is saved in localStorage under
ysll-tap-settings (saveSettings() / loadSettings()), and mentioned in privacy.html. Nothing is sent anywhere. Number keys 1-8 pick a choice.
Results also offer "Make a writing practice sheet": checkboxes pick the letters (missed question letters, wrong picks,
their hiragana/katakana conversions, optionally all), a boxes-per-letter number and the PDF language, and "Generate PDF"
renders it right on this page (generateSheet() uses HiraganaPracticeRenderer in custom mode, so this page also loads
pdf-lib, pdf-common, pdf-lang, kana-safety and the practice generator). Practice boxes come in steps of 5 (a 5-25 dropdown on both pages, normalizePracticeRepeats()), and the renderer keeps one character's columns on one page (a character that doesn't fit in the rest of a page starts a new one). A small link still opens
japanese-practice.html?mode=custom&chars=...&lang=.. for title / teacher options.
No ad slots on this page yet.kana-online.html / js/i18n-ja-online.js - the Online Learning page (オンライン学習), a hub like japanese.html with no ads and no
sub-nav. Online Learning lives under the Japanese section: there is no top-bar button for it (the header's "Japanese" key is the
active one on every online page), it is linked from a section of japanese.html (heading hub.online.h, cards for Tap Practice and
Word Puzzle), from a "Japanese: online learning" section on index.html, and from a sub-nav tab (tab.online) at the end of the
printable Japanese pages. The online pages' own sub-nav is Online Learning / Tap Practice / Word Puzzle / Word Search. Rule: printable
sheets, charts and the plan stay in the Japanese hub; anything that runs on screen goes in Online Learning. A new online tool needs a
card on kana-online.html, japanese.html and index.html, a tab in the online sub-nav, and a sitemap entry.kana-word-puzzle.html / js/kana-word-puzzle-generator.js / js/kana-word-puzzle-ui.js / js/i18n-ja-wordpuzzle.js - an
on-screen (not printable) crossword played by drag and drop. No picture clues and no spare tiles (both were tried and dropped).
KanaWordPuzzleGenerator.build() places 5-10 words (default 7; the grid may grow to 10x10 squares) from wpLibrary() (every HIRAGANA_WORDS entry plus the five-kana words in WP_FIVE_KANA_WORDS, of 2+ kana, that the
safety list accepts): the first word across, then random words crossing a placed word at a shared letter (_fits() lets a new word
touch others only by crossing, so no accidental words appear; tested by reading every grid run), at most 8x8. Some letters of each word
start filled in ("Letters shown": few = 1 per word, some = half, many = all but 2; every word keeps at least one empty square). The tiles
are exactly the letters the empty squares need (a shared letter is one tile), shuffled, and the puzzle is redrawn if the tiles could spell
anything on the safety list. A puzzle is solved when every across and down run is a word from the list (puzzle.dict), not only the
intended one, so another valid arrangement also solves it; a full run turns green when it is on the list; a full run that is not on the list is not marked at all (no red, no message), so the student gets no hint. A move that would spell something on the safety list is refused (wpBlockedAfter()). Nothing is locked except
the given letters and hinted squares. Dragging uses Pointer Events (mouse, touch and pen): press a tile (or a tile already in the grid)
and move more than 6px and a copy follows the pointer (wpDragStart/Move/End), letting go over a square drops it there (a tile already
there goes to the dragged tile's old spot), over #wp-tray returns a grid tile; press-and-release is a tap (tap a square, then a tile);
Enter/Space on a button works as a tap. Tiles and squares are touch-action: none so a drag does not scroll the page. "Hint" fills the
chosen square. A tile plays its sound when it is picked up (sound is off until ticked) (audio/kana/<romaji>.wav for the 46 basic kana, the browser's Japanese voice for the rest or
on failure) (a finished word is not read out). Scoring is one point per letter (wpWordPoints(), shared with the Word Builder), so はし is 2 and
はしら is 3; score = points of the words that are right minus 1 per hint used (never below 0). Tiles carry no points. Nothing is stored. All state is in wp,
rendered by wpRender(). Needs japanese-practice-generator.js, kana-safety.js and kana-matching-generator.js (MATCH_ROWS, for the romaji
of a clip).kana-word-search.html / js/kana-word-search-generator.js / js/kana-word-search-ui.js / js/i18n-ja-wordsearch.js - an on-screen word search. KanaWordSearchGenerator.build() plants 3-15 words ("Hidden words", default 10; word lengths are picked evenly so short words are not crowded out) from the simple example words of wpLibrary() (HIRAGANA_WORDS only, 2-4 kana; plantable), plus one long word (5+ kana, from longWords) when the grid is 5+ wide and 3+ words are hidden, two when 6+ are hidden in straight lines in a 4x4 to 10x10 grid (default 7x7) (directions: easy = across and down, normal = plus diagonals, hard = all 8 directions; WS_MODES), fills the rest with random kana, then repairs the filler: any run containing a KANA_BLOCKED_WORDS entry (checked in every line, both ways, whatever the mode) or a list word that is not planted gets a filler square re-rolled. A list word made only of planted squares (りす inside きりぎりす) stays, and any list word in the grid counts when found. The game ends ("Cleared!") when the "Words to find" number of words (cfg-goal) (1-10, default 5, capped at the hidden count) is found, so the student chooses between short and long words. If the words don't fit, fewer are planted and the page says so. The UI is tap-only (no drag): taps must follow one straight line (the second tap sets the direction, each later tap continues it), tapping an already picked letter drops it and the letters after it (there are no Undo/Clear buttons), and Submit checks the string against puzzle.valid. Letters of found words turn green (darker when shared) and can be reused; a found word can't score twice. One point per letter, Hint (marks the first square of an unfound word) costs 1. Found words are checked against a larger list, KANA_SEARCH_DICT in js/kana-word-dictionary.js (common JMdict nouns of 2-6 kana, filtered like data-pipeline/build.js; generated by node data-pipeline/build-search-dict.js, then filtered again by kanaWordBlocked at load), so everyday words such as うた are accepted even when the teaching list lacks them. Sub-nav tab tab.search; cards idx.search.* on kana-online.html, japanese.html, index.html.kana-search-sheet.html / js/kana-search-sheet-generator.js / js/kana-search-sheet-ui.js / js/pdf-lang-searchsheet.js / js/i18n-ja-searchsheet.js - the printable word search, built from the same KanaWordSearchGenerator as the online one (grid 6-12, hidden words, "Words to find" goal, directions, 1-3 puzzles). KanaSearchSheetRenderer extends KanaPuzzleSheetRenderer (which now takes its header text keys from this.keys): title/name/date header, "Find at least N words", the grid as large as fits, then numbered answer lines with a points box and a total. The key page is labelled "Example Answers" / こたえの例 (other real words count too): it rings each hidden word with a rounded outline along its direction (_drawRing) and lists the hidden words with letter counts. The sheet has no word list, so students pick words themselves; tab tab.searchsheet (on the Puzzle Sheet and this page), cards idx.searchsheet.* on japanese.html and index.html.kana-word-builder.html / js/kana-word-builder-generator.js / js/kana-word-builder-ui.js / js/i18n-ja-words.js - the printable word
sheet (Japanese section, sub-nav tab tab.builder), reusing wpLibrary() and wpWordPoints() from kana-word-puzzle-generator.js (which
this page therefore loads). The page says "kana", never "tiles". Nothing but the title, teacher and answer key is adjustable: every sheet has
2 puzzles side by side as vertical panels (WB_PUZZLES, dashed divider, 30pt page margin, 14pt between panels), each a set of 10 different kana
(WB_RACK) that asks for 4 words in 5 boxes each (WB_COLUMNS, WB_BOXES; 5 fits the longest list words). A set holds only plain kana (no
dakuon) and is built around a random five-kana word with five different plain kana (WP_FIVE_KANA_WORDS joins Writing Practice's example
words in wpLibrary(); wbBase() strips the dots), the rest filled with whole list words that fit the room left (60% of the time) or single plain
kana, never one the set already has (no duplicate kana); tried up to 400 times for 7-16 list words the set can make. Fairness: 14 candidate
sets are drawn and buildPuzzles() picks the pair with the smallest difference in the number of buildable list words and in the points of their four
best words (and at least WB_MIN_WORDS), so neither puzzle has fewer words to find. A set is refused if any word on the safety list could be built from
its kana (dots and free letters added), and the order is reshuffled so it never reads as one. Forgiving rules (printed on the sheet, listed on the
page; worded "can also be used to create words", and the dots are called dakuon, not "dots"): the small kana (WB_SMALL, folded: ぁぃぅぇぉゃゅょゎっ) and ー can
also be used in any word, dakuon can also be used (with か in the set a student may use が, with は ば or ぱ), and a word may be written in katakana. wbDictionary() is the list the answer key draws on: the shared list plus WB_EXTRA_WORDS
(hiragana words with small kana or ー, and katakana loanwords), five letters or fewer, each word once in either script; wbCore() folds a word to hiragana,
drops the free small kana and takes the dots off, and a word is buildable when its core is within the set. Points are one per written letter (small kana
and ー included), so はし is 2 and はしら is 3; tiles carry no points. The preview's "Shuffle" button builds new sets. One worksheet page (the panels start
below however many header lines the rules need), no answer QR; an optional key page lists the buildable words, longest first. Like Kana Matching, the
preview's sets are cached and passed straight to . PDF text is in (, kana-only Japanese, no font-subset
rebuild needed).kana-puzzle-sheet.html / js/kana-puzzle-sheet-generator.js / js/kana-puzzle-sheet-ui.js / js/pdf-lang-puzzlesheet.js /
js/i18n-ja-puzzlesheet.js / js/i18n-zh-puzzlesheet.js - the printable Kana Word Puzzle (Japanese section, sub-nav tab
tab.puzzlesheet, hub card idx.puzzlesheet.*). The same crossword as the online puzzle: it reuses KanaWordPuzzleGenerator, whose
constructor takes wordsMin / wordsMax / maxSide overrides (the sheet uses 7-15 words, default 10, on a grid of at most 10 x 10
squares, or 12 x 12 for 14-15 words, so boxes stay big). The generator never leaves a one-square gap (two squares in a line with
exactly one empty position between them, which reads as a missing box): _makesGap() rejects such placements while building, and if the
words will not fit build() lets the grid grow a little (up to +4) instead of dropping words. One puzzle per page (1-3, default 1), A4:
header, a row of boxes holding the kana to place (to cross out), then the grid with every box as large as the page allows (PS_BOX_MAX
72pt; a grid wider than tall is drawn turned on its side when that makes the boxes bigger, since a crossword reads the same sideways).
Given letters are grey boxes, empty boxes carry a faint crosshair. "Letters shown" few / some / many as online. Answer key: one page per
puzzle, grid filled in. The preview puzzles are cached and passed straight to KanaPuzzleSheetRenderer. PDF strings (psheet.*) are
added to PDF_TEXT by pdf-lang-puzzlesheet.js (load after pdf-lang.js); the Chinese ones needed new characters, so
NotoSansSC-Regular.ttf was rebuilt (see the font notes below).japanese-practice.html / js/japanese-practice-generator.js / js/japanese-practice-ui.js - a separate,
not-ported-from-Python tool: hiragana handwriting/tracing practice (single character - via a dropdown, not a
text field, since typing Japanese isn't a given on every device - a whole row/行, or all 46 basic hiragana).
No stroke-order arrows (accurately sourcing stroke-path data for 46+ characters wasn't worth the risk of
silently-wrong data) and no answer key / QR / matching-code concept - tracing has nothing to "check". Trace
boxes are laid out the way Japanese is actually written: each character gets its own column of up to 5 large
boxes read top-to-bottom, and columns fill the page right-to-left (render()'s colRightX cursor in the
generator starts at the right margin and decreases) - a whole 行 (5 characters) at the default 5 repetitions
each fills exactly one page edge-to-edge, one column per character, in their natural reading order. Single-
character mode defaults to 15 repetitions instead (3 columns), using the leftover width for an optional
example-word writing line - row/all mode has no such leftover space, so that option is disabled there. The
model reference character at the top of each character's first column is a full box-sized cell (not a small
aside label) and, like every faint trace-hint inside the boxes below it, is centered on the box with
_drawCenteredJp() - measuring the actual glyph width via font.widthOfTextAtSize() rather than a hand-
guessed offset. Example words reuse that exact same box-and-crosshair rule too: each character of the word
gets its own box-sized cell in a vertical column (shrunk together, never dropped, if both words don't fit the
leftover width at full size), not one word squeezed as plain text into a single wide box.
The "Practice word" fieldset (single-character mode only) also takes up to 2 user-typed words of up to 5 kana
(parsePracticeWords(); used instead of the built-in HIRAGANA_WORDS when any are entered). Input is limited to the
kana present in the font subset; ー isn't in it, so _drawTraceBox() draws it as a vertical bar (the boxes read
top-to-bottom like vertical writing).
Every typed word must contain the selected character in either script (isPracticeTarget(): ろ accepts メロン via
toKatakana()), anywhere in the word. "Hide the other letters" renders the word column as 'blanks' (only the
selected character shown, solid; other boxes open) and appends a page 2 with the whole word as the sample answer
('answer' view). The header uses the Japanese font when a typed title/teacher isn't Latin-only.
Cross-linked with kana-quiz.html via a small sub-nav row under each page's <h1>.cfg-mode=custom, text box #cfg-custom, parseCustomChars() in the generator) takes any
typed/pasted hiragana or katakana (basic 46 plus dakuten/handakuten, VOICED_KANA), kept exactly as typed so scripts can mix
(the script radios are hidden and the PDF title defaults to practice.defaultKana), de-duplicates, keeps typed order, and lists other
characters as left out. applyIncomingSetup() in the UI fills it from the URL params above and scrolls to the preview.kana-study-plan.html / js/i18n-ja-plan.js - a guide page (no tool): a kana curriculum that walks through the
three Japanese tools in order (one new letter a day in Writing Practice, a weekly Kana Matching sheet for the
finished rows, katakana the same way, then the Conversion Quiz in order and then randomized daily, with missed
letters fed back through "Always include" and a point-and-say reading check, plus a "No time for a quiz?"
section that offers kana-tap-practice.html as the busy-day stand-in and quotes its Question / Choices /
Practice missed only / Make a writing practice sheet labels). Linked from the Japanese tools'
sub-nav (tab.plan) and the home page. It names real button labels, so update it when those change.
Its chart steps link to kana-chart.html.kana-chart.html / js/i18n-ja-chart.js - hiragana and katakana charts (46 basic letters, no tool, no ads),
sub-nav tab tab.chart. Traditional sideways layout: 11 columns read right to left (あ行 on the right, ん on the
far left) by 5 rows (あいうえお), built by the page's inline script (katakana = hiragana + 0x60); each letter is a
keycap tile in the site's five colorways, the や/わ gaps are dashed empty cells. Three dropdowns pick the big
letter (Main) and up to two small ones under it (Sub 1, Sub 2; each hiragana / katakana / romaji, subs also
None), defaulting to hiragana / katakana / romaji. Picking a script another dropdown shows swaps the two (if the
old value was None and the other is Main, Main takes the unused script instead); presets that repeat a script are
fixed on load. URL
params ?main=&sub1=&sub2= preset them (the older ?script=kata = katakana main, hiragana sub). Setting both subs
to None is the point-and-say mode. "Print this chart" prints only the chart card on one landscape page, under a
print-only header (logo, "Learning Lab", and a QR code to the home page made with qrcode-generator), sized in cqw
of the printed page (main is the print container) so it never pushes the chart onto a second page.
@page { margin: 0 } (the body padding is the print margin) keeps the browser from adding its date/URL lines.
A Paper dropdown (A4 default, A3, JIS B4, Letter, Tabloid; ?paper= presets it) rewrites a <style id="page-size">
with @page { size: <paper> landscape; margin: 0 }; the chart scales to whatever paper is chosen. Every chart size is
in cqw of the chart's own width, so the printout is the on-screen chart scaled to the paper - don't add
separate print font sizes (fixed sizes overflowed the square cells and split the chart over two pages).js/pdf-common.js - shared page geometry constants, font loading, drawing helpers, download triggerassets/fonts/NotoSansJP-Regular.ttf - embedded Japanese font: a fontTools subset (~330 glyphs, ~60KB) of the
Noto Sans JP variable font (SIL OFL; licence name records are kept), instanced at wght 400 and subset to: ASCII, all
hiragana and katakana, ー and a few Japanese punctuation marks (。、「」・!?()~:→), and a small set of
common worksheet kanji (kept so a typed title or teacher name like 先生 still prints; the built-in Japanese PDF text in
js/pdf-lang.js is kana only). Embedded with { subset: false } in js/pdf-common.js (the fetch URL carries a
?v= so browsers pick up a new build) - pdf-lib/fontkit's own in-browser subsetter is unreliable on large CJK
fonts, so the font is pre-subsetted offline. A character missing from the subset prints as a blank box, so
whenever you add Japanese text to PDF_TEXT.ja (or any PDF string), rebuild the subset: download
NotoSansJP[wght].ttf from the google/fonts repo into its own folder, then with fontTools
(fontTools.varLib.instancer.instantiateVariableFont(font, {'wght': 400}), then fontTools.subset.Subsetter with
layout_features = [], name_IDs = ['*'], hinting = False) keep the union of the current font's cmap and the new
characters. The 2026-10 rebuild kept every existing glyph byte-for-byte identical in bounds and advance. Check
coverage with fontkit's glyphForCodePoint(...).id !== 0 for each character in PDF_TEXT.ja.assets/fonts/NotoSansSC-Regular.ttf - Simplified Chinese PDF font (~1.2MB): a fontTools subset of the official
NotoSansSC[wght].ttf from the google/fonts repo (SIL OFL; licence name records kept), instanced at wght 400 and
subset to ASCII, all kana, CJK punctuation, full-width forms, every character in the JP font and in js/pdf-lang.js,
and all 3,755 GB2312 level-1 hanzi, so typed Chinese titles and names print. Embedded only when the PDF language is
zh (getFonts(pdfDoc, lang) puts it in the notoSansJP slot), so Chinese PDFs are ~800KB. Rebuild with the same
fontTools options as the JP font if PDF_TEXT.zh gains a character outside GB2312 level 1.js/i18n-zh-*.js mirror js/i18n-ja-*.js key for key (loaded right after each ja file); zh is in
SUPPORTED in js/i18n.js and js/i18n-boot.js. The math page stays English (it shows the "coming soon" notice).js/pdf-lang.js - the text printed on PDFs (PDF_TEXT.en / .ja / .zh) and the "PDF language" dropdown
(#pdf-lang, under each Generate button). The dropdown starts on the page language (so ?lang=ja gives a Japanese
PDF) and keeps following it until the user picks one. A worksheet title left at a built-in English default follows
the PDF language; a title the user typed is printed as typed. Used by the quiz, matching and writing-practice
renderers; the math tool isn't translated yet. Japanese strings use ASCII punctuation (": ", "!") where possible.results.html / js/pdf-viewer.js - shared PDF viewer page every "Generate" button opens in a new tab
instead of downloading directly (renders the PDF with pdf.js, thumbnail rail + Print + Download + Share; no ads,
noindex). Share (wireShare()) uses the Web Share API with the PDF as a file, so the device's share sheet
(Mail, Gmail, Messages...) sends it as an attachment, with nothing uploaded; browsers that can't share files get a download plus a
blank mailto: draft. See "Adding a new worksheet tool" below.answers.html / js/answers-viewer.js - decodes and displays the answer QR code's payload (see
"Answer-key QR code" below). No IndexedDB handoff like results.html - everything needed to
render the page arrives in its own URL, since the QR is meant to be scanned on a phone with no prior visit
to the site.assets/css/site.css - small set of utility classes Tailwind's CDN build can't express (dashed ad placeholders,
bold worksheet index numbers, crosshair answer boxes)Just open index.html in a browser, or serve the folder with any static file server
(python -m http.server, VS Code Live Server, etc.) - opening math.html/kana-quiz.html
directly via file:// also works since there's no backend.
main / root.The site is linked to AdSense (publisher ca-pub-6130154285914649, verified via the <script> tag in every
page's <head> and /ads.txt at the site root).
Current placements (keep these review-safety rules when adding ads):
index.html: one leaderboard (#ad-slot-bottom-leaderboard, slot 7285905093) below the tool cards and
the How it works / FAQ content.math.html, kana-quiz.html, kana-matching.html, japanese-practice.html, kana-word-builder.html, kana-puzzle-sheet.html, and kana-search-sheet.html: two ads each -
#ad-slot-below-preview (slot 7285905093) at the bottom of the preview card, and #ad-slot-in-article (slot
7199305456) between the 2nd and 3rd sections of the guide article below the tool.kana-word-puzzle.html and kana-word-search.html: one #ad-slot-in-article each (slot 7199305456). Their
below-game slots are removed during the approval review and must not return next to game controls.kana-study-plan.html: one #ad-slot-in-article (slot 7199305456) between Step 4 and Step 5.results.html, answers.html (utility screens, also noindex), about.html, privacy.html,
or contact.html.Rules these placements follow:
order-* classes), not just the desktop grid.about.html, privacy.html (required by AdSense: discloses Google's ad cookies), and contact.html are linked
from every page's footer. robots.txt and sitemap.xml are at the site root; add any new indexable page to
sitemap.xml.
Do not cap an ad slot's height or hide overflow: a responsive creative can be taller than its wrapper and must
never be cropped. Unfilled units collapse through the site.css rule
ins.adsbygoogle[data-ad-status="unfilled"].
Every ad slot is a plain <div> wrapping one <ins>/<script> pair, never inside a form or the preview grid
itself, so ad content can't overlap form fields or generated worksheet content. To wire up a new/replacement ad
unit, paste its snippet in place of the existing <ins>/<script> pair, e.g.:
<div id="ad-slot-below-preview">
<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-XXXX" data-ad-slot="XXXX"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
Every "Generate" button opens the finished PDF in results.html (a new tab, ad-free PDF.js viewer) rather
than downloading it directly. results.html has no idea what subject generated the PDF - it only knows how to
render whatever bytes it's handed - so a new tool (e.g. a future writing-practice generator) gets this for free
by following the same three-step contract used by math-ui.js/japanese-ui.js:
await, call const resultTab = window.open('', '_blank'); -
this must happen synchronously in direct response to the click, or the browser's popup blocker may treat a
tab opened after your (async) PDF generation finishes as not user-initiated and block it.await openPdfResults(bytes, filename, '<your-tool>.html', '<Tool Label>', resultTab); (from
js/pdf-common.js) instead of downloadPdfBytes(...). It hands the bytes off via IndexedDB and points
resultTab at results.html?id=...; if IndexedDB is unavailable it falls back to a direct download so
generation never breaks.No changes to results.html or js/pdf-viewer.js are needed for a new tool.
Every worksheet page (not the answer-key pages - they already have the answers printed) can carry a second QR
code, in the header right under the worksheet-matching code (top-right), alongside the existing homepage QR at
bottom-right (which now has a small "Print more!" caption). Toggle: #cfg-answerqr on both math.html and
kana-quiz.html, config.include_answer_qr, default on. Deliberately placed up in the header rather than the
bottom margin - a bottom-margin QR sits right where a student's hand rests while writing, risking a smudge that
makes it unscannable.
Since this whole site is static with no backend, the QR can't look anything up server-side - instead it encodes
a link to answers.html with the answer data itself packed into the URL: buildAnswerQrUrl({ c, t, a }) in
js/pdf-common.js builds answers.html?c=<code>&t=<title>&a=<base64url(answerString)> - only the answer list
itself is base64'd (unavoidably non-ASCII for Japanese), the code/title travel as plain query params, and there's
no JSON wrapper (its braces/quotes/colons would otherwise get percent-escaped to 3x their size). js/answers-viewer.js
decodes it entirely client-side and renders a plain answer list (it also still understands the older single
?d=<base64url JSON> format, so an already-printed worksheet's QR doesn't dead-end). This targets parents
helping with homework: scan once, see the answers, no need to track down the teacher or dig up a saved PDF, and
no server cost since it's the same static hosting as everything else on this site.
Design notes - getting a QR that's actually legible on a phone at print size turned out to be the hard part, not the encoding:
_answerSummary() in
math-generator.js) for math, and for Japanese just the target-script answer with no prompt half - a
plain comma list in grid reading order (left-to-right, top-to-bottom). Every kana character costs 3 UTF-8
bytes, so dropping the redundant prompt roughly halves that payload - it matters far more for scannability
here than it does for math's plain-ASCII digits. This only works because every kana cell is now stamped with
a small bold index number (Times-Bold, same convention as the math tool's "1.", "2." problem numbers) - the
Japanese grid used to carry no printed numbering at all, so an answers-page list read "Q1, Q2, ..." with
nothing on the actual worksheet for a student to match it against. The cell numbers and the QR/answers-page
numbering are the same reading-order sequence, so "Q1" on the answers page now always means the box printed
"1". The number is drawn just outside the answer box's top-left corner, in the margin between the box and
the cell edge - never inside the box (that would eat into the one place a student needs full room to write)
and never stacked above the prompt (the cell's vertical budget has no slack for that). boxSize is capped at
cellWidth * 0.75, so there's always at least cellWidth * 0.125 of margin on each side by construction;
the number shrinks to fit whatever sliver that leaves (down to 5pt on a narrow many-column grid) rather than
assuming a fixed size. The prompt is drawn slightly smaller (20pt -> 17pt) purely for a little clearance
above it; the answer box keeps its exact original size - shrinking that would make it cramped to
actually write a character in.'L' (least redundant) instead of the default 'M' - fewer
redundant bits means a smaller QR version for the same data, which is what actually determines module count.sizeQrForData() in js/pdf-common.js builds the real QR object first, then sizes the box so each module
lands near a target size (~1.3pt/~0.46mm), clamped to what the header has room for (46-64pt). If the payload
is so dense that even the max size can't keep modules above a legibility floor (~0.85pt/~0.3mm), the QR is
skipped entirely rather than drawn too small to scan - the homepage QR and the full printed answer-key page
are always still there as a fallback.js/kana-safety.js holds one shared list of clearly inappropriate words (insults, slurs, crude and sexual words), used by
every kana tool: the quiz grid scan below, the Kana Matching columns (each column, read top to bottom, is re-drawn until it
spells nothing on the list) and the Writing Practice word boxes (a typed word on the list is refused with a warning).
Entries are hiragana; katakana and the long-vowel mark are folded away before comparing. The list is stored base64-encoded in
KANA_BLOCKED_WORDS so the words never appear as readable text in the served site (ads / search indexing). The plain list is
kept outside the repo (../Ys School private/blocked-words-plain.txt). To edit: change that file, then re-encode with
base64 of its UTF-8 content (newline-separated) and paste the result into the atob('...') call.
Kana are placed on the grid randomly, so nothing inherently stops two or three adjacent cells from spelling
something inappropriate when read left-to-right or top-to-bottom - a real risk once a grid gets big enough
(measured empirically at ~9% of trials on a dense 10x5 mixed-direction grid before this guard existed).
RandomizationEngine._sanitizeGrid() in js/japanese-generator.js scans the finished grid for any
horizontal/vertical run matching KANA_ADJACENCY_BLACKLIST (the shared list above) (checked via each cell's hiragana reading,
regardless of whether the grid displays katakana or hiragana prompts, since they read the same either way) and
swaps one cell in the offending run with another random cell elsewhere in the grid - only keeping the swap if
it doesn't just create a new violation at either swapped position. It's a short, curated list of clearly
inappropriate items, not general profanity/slang filtering, and it only runs in the randomized/mixed-order
path - sequential ("in order") mode lays out a fixed canonical a-i-u-e-o / ka-ki-ku-ke-ko layout that swapping
would break, and which never spells anything problematic in that order anyway.
eval()/new Function() anywhere. The math tool's mixed-continuous-equation feature (which used Python's
eval() on a string) is replaced with evalTokens() in js/math-generator.js, a small recursive-descent
evaluator operating directly on the token array (numbers/operators/parens) with standard precedence.combined_pdf=True default path..worksheet-index in assets/css/site.css), so they're never
confused with equation digits.y=20 so it clears the paper edge even on printers with a tight unprintable margin - a partially-clipped QR
code is unscannable, unlike text, which degrades more gracefully; drawQrCode() in js/pdf-common.js)
linking back to the site's homepage - drawn as plain filled rectangles from the
qrcode-generator CDN library's module matrix, not a
raster image, consistent with how everything else in these renderers is hand-drawn. Wrapped in a try/catch so
a CDN hiccup degrades to "no QR code" rather than breaking generation.XRGD0831-A-1 (getWorksheetBatchCode() in js/pdf-common.js) - 4 random uppercase letters plus today's
date (MMDD), freshly generated per "Generate" click, with a -A-N page suffix. Embedding the date means a
code from one day's print run can never be confused with a code from a different day using the same settings
(a real classroom scenario: the same worksheet config run daily would otherwise print the same code every
time). The same per-page code appears on both the worksheet page and its corresponding answer-key page so a
teacher can pair a stack of student worksheets to their answer keys by eye.Every <script src="js/..."> and the site.css link carries a ?v=N query param. Bump it whenever you edit
that file - browsers cache these aggressively with no other cache-control here, and without bumping the
version, visitors (and you, testing) can silently keep running old JS/CSS after a deploy. Cache-busting is
per-file-type, not global: all js/*.js references share one number (?v=18 currently), site.css has its own
(?v=8 currently) - bump whichever group you actually touched.
KanaWordBuilderRendererjs/pdf-lang.jswords.*answers.html renders fully untrusted input. Its query params are attacker-controllable (anyone can craft
a link), so js/answers-viewer.js HTML-escapes every decoded value before inserting it into the page - never
pass decoded payload content through innerHTML unescaped if you touch this file.PDF_PAGE.HEADER_HEIGHT150#cfg-title/#cfg-teacher on both
math.html and kana-quiz.html, threaded through as config.title/config.teacher) instead of hardcoded text -
Title defaults to "Math Practice Test" / "Kana Practice Test" and is printed as-is (with " - Answer Key"
appended on the math tool's answer-key pages); Teacher prints the typed name directly when given, or falls
back to a blank fill-in line like Name/Date/Class when left empty. Both round-trip through INI export/import.#cfg-footer / config.show_settings_footer, MathPDFRenderer._settingsSummary() in
js/math-generator.js) - a small gray line summarizing layout/mode/randomization, and the "Powered by..."
footer now also states PRACTICE SHEET vs ANSWER KEY like the Japanese tool's does.