In short
A Japanese school drill book: sums, number boxes, choices and true-or-false on one page, kanji and readings on another, set top to bottom. Under every exercise its answers are printed upside down, so a child turns the book to check.
What you'll build
Two pages from a third-year drill series on B5. The arithmetic page runs across: a pale blue band with the subject badge, the title and a tanuki mascot, a name line and a score box, then ひっ算 with a box for each digit of the answer, number boxes after six sums, ア イ ウ choices in a row, ○× statements with a bracket at the end of the line and two word problems with lines to write the working on. The kanji page is set the way a 国語 drill is printed, top to bottom and right to left: readings with a box for the kanji, lined words whose reading goes in brackets, choices to ring and a meaning to pick. Under every exercise, its answers are printed upside down in small grey type. A child sees where they are, cannot read them at a glance, and turns the book round to check.
This recipe answers
- How do I print the answers upside down at the foot of an exercise?
- How do I set a Japanese drill page in vertical columns with the same exercise design as the horizontal one?
- How do I set long division the way each country's schools write it?
The short answer
// `placement: 'inverted'` sets each exercise's key right after it in a box turned 180°: the
// pupil sees where the answers are without reading them, and turns the book to check. The
// key's words come from the exercise (the options' katakana, ○ and ×, the operations), so
// the Markdown holds nothing twice.
const drill = {
id: 'drill',
number: { shape: 'square', fontFamily: TEXT, fontWeight: 600, background: col('blue') },
instruction: { fontFamily: ROUND, bold: false },
options: { layout: 'row' }, // ア イ ウ side by side (the default labels in Japanese)
trueFalse: { look: 'bracket' }, // ( ) for ○ or ×
blank: { look: 'box', width: 'answer', color: col('rule') }, // □ to write in
area: { kind: 'lines', color: col('rule') },
grid: { rule: col('rule') },
solutions: { placement: 'inverted', box: 'kotae' },
marginTop: em(1.1), marginBottom: em(0.4),
};
const kotae = { id: 'kotae', title: '', backgroundEnabled: false, border: { enabled: false },
padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
body: { fontFamily: ROUND, fontSize: pt(8), lineHeight: pt(12), color: col('muted'),
firstLineIndent: pt(0) },
marginTop: mm(1), marginBottom: mm(2), keepTogether: true };
The answers printed upside down under each exercise
Ingredients
- Features
- SolutionsExercise stylesVertical textExercisesOperations in columnsGaps and word banksChoice and true or falseOpen questions and writing areasKana, kanji and rōmajiChinese, Japanese and Korean fontsSide lines (bōsen)Books bound on the rightInline chipsThree or more columnsDesigned openersHeading attributesPictures in page designsAnchoring design elementsSemantic colour palettePDF export
- Also uses
- Columns in a box or in the textBoxes that split or stay wholeChinese line breakingColumn balancingParagraph stylesFonts embedded in the PDFFigures and tables as resources
- Type
- Klee One, Zen Maru Gothic (SIL OFL 1.1)
- Assets
kitsune-600.jpgtanuki-600.jpg- A young tanuki in a yellow school hat holding a big pencil. (Generated With Diffusion Models, original)
- A fox cub in a red scarf reading a book. (Generated With Diffusion Models, original)
Method
#1 · Turn each exercise's key upside down
The code is the short answer above. placement: 'inverted' sets the key after each exercise in a box turned 180°, and the box goes inside the exercise's own box, so a page break never parts the two. The key is made from the exercise: the operations with their results, the katakana of the right option, ○ and ×, the words of the gaps. The answers box style, kotae, only sets the type small and grey.
#2 · One band for a page across and a page down
// A heading design on a vertical page is laid along its column: x runs down the page and y
// to the left, so the same band stands at the right edge, from head to foot.
const BAND = 30; // mm: the band's depth (its height across, its width down)
const header = { enabled: true, minHeight: mm(BAND + 3), slot: { elements: [
{ kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint'),
borderRadius: mm(4) }, placement: { anchor: { to: 'container', edge: 'top-left' },
size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'image', id: 'mascot', resourceId: '{attr.pic}', decorative: true, reserve: false,
placement: { anchor: { to: 'container', edge: 'top-right' }, offset: { x: mm(-4), y: mm(1) },
size: { width: mm(28), height: mm(28) } } },
{ kind: 'text', id: 'subject', content: '{attr.kicker}', fontFamily: ROUND, fontSize: pt(10),
fontWeight: 700, color: col('ink'), box: { backgroundColor: col('sun'), borderRadius: mm(3),
padding: { top: mm(0.6), bottom: mm(0.6), left: mm(2.4), right: mm(2.4) } },
placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: mm(5), y: mm(5) } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: ROUND, fontSize: pt(20),
fontWeight: 700, color: col('ink'),
placement: { anchor: { to: '#subject', edge: 'below' }, offset: { x: mm(0), y: mm(2.5) } } },
] } };
A heading design on a vertical page is laid along its column: x runs down the page and y runs to the left. The band anchored top-left with the full width stands at the top of the arithmetic page and down the right edge of the kanji page; the mascot anchored top-right sits at the band's far end on both. Nothing in the design names a direction.
#3 · Two drills from one text
// Two drill books of one series: arithmetic across, bound on the left; kanji in columns,
// bound on the right (vertical text binds there by itself).
const [sansu, kokugo] = markdown.split(/(?=^# 春のことば)/m);
const build = (text) => withLoadedFonts(() => buildDocument({ markdown: text, resources },
config()), { ...kitFonts(FONTS), text: markdown });
const across = await build(sansu);
// 国語 is set in columns, top to bottom, right to left: the same design, turned.
writingMode = 'vertical-rl';
const docs = [across, await build(kokugo)];
The configuration is a function, so each build gets its own object, and only writingMode changes between the two. Set vertically, the 国語 drill binds on the right by itself; showBook lays each drill out on its own side of the spine. The text is set in Klee One, a textbook face, and the labels in Zen Maru Gothic; both load their vertical punctuation for the brackets of the kanji page.
The whole recipe
// ═══ Postext Cookbook · Nº 172 · A Japanese drill with the answers upside down ═══
// https://postext.dev/en/cookbook/japanese-drill-upside-down-answers
// Code: MIT · Text: original (CC BY 4.0) · Pictures: generated with diffusion models
// Fonts: Klee One, Zen Maru Gothic (SIL OFL 1.1) · Needs postext ≥ 1.59.0
import {
buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage,
loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
const LANG = 'ja'; // @lang: the language of the frame; the drill is Japanese in all
const RECIPE = 'japanese-drill-upside-down-answers';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a drill book's blue, a warm yellow and a pale sky for the band
const palette = {
ink: '#23272c', // text
blue: '#2a6cb0', // the accent: exercise numbers, the title band's rule, the score box
sun: '#f2c14e', // the subject badge
tint: '#eaf2fb', // the title band
rule: '#a8bfd8', // writing lines and boxes
muted: '#69717b', // folios, the name line, the upside-down answers
paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
{ id: 'main-color', name: 'accent (defaults)', value: { hex: palette.blue, model: 'hex' } },
];
// #endregion
// 教科書体 for the text a child copies from, a round gothic for the labels.
const [TEXT, ROUND] = ['Klee One', 'Zen Maru Gothic'];
// #region answer: the answers printed upside down under each exercise
// `placement: 'inverted'` sets each exercise's key right after it in a box turned 180°: the
// pupil sees where the answers are without reading them, and turns the book to check. The
// key's words come from the exercise (the options' katakana, ○ and ×, the operations), so
// the Markdown holds nothing twice.
const drill = {
id: 'drill',
number: { shape: 'square', fontFamily: TEXT, fontWeight: 600, background: col('blue') },
instruction: { fontFamily: ROUND, bold: false },
options: { layout: 'row' }, // ア イ ウ side by side (the default labels in Japanese)
trueFalse: { look: 'bracket' }, // ( ) for ○ or ×
blank: { look: 'box', width: 'answer', color: col('rule') }, // □ to write in
area: { kind: 'lines', color: col('rule') },
grid: { rule: col('rule') },
solutions: { placement: 'inverted', box: 'kotae' },
The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)
Variations
#Collect the answers at the end
A drill printed with a separate answer booklet keeps the key out of the pages: chapterEnd lists it after the last exercise.
- solutions: { placement: 'inverted', box: 'kotae' },
+ solutions: { placement: 'chapterEnd', box: 'kotae' },#Print a puzzle page
The puzzle page turns its answers the same way, on cards in two columns.
Pitfalls
Pitfall
Chinese faces load by slices, through the cjk block
Fontsource serves a Chinese, Japanese or Korean family as about a hundred files per weight, each covering a range of characters. loadFonts fetches only the latin file, so on screen the Han characters come from a system face and measure wrong, and fontsourceProvider hands the PDF that latin file, which prints them as empty boxes. List the cjk kit block, call loadCjkFonts(FONTS, markdown) after loadFonts (once per voice, with the text it sets, when the book uses several CJK faces) and give renderToPdf fontProvider: cjkPdfProvider: both take the files that hold the text's characters. Chinese, Japanese and Korean fonts →
Pitfall
Tag a Japanese text 'ja', never zh-Hans or LANG
A recipe's editions are en and es, but a Japanese sample is Japanese in both: `locale: LANG` would tag it English or Spanish, and a Chinese tag would set it by the Chinese rules (Kaiming punctuation, small kana free to start a line, 图 for 図, Chinese glyph forms in the PDF). From postext 1.30 the tag is also the language the text is shaped in (measured, painted and embedded), so any tag but 'ja' gives other glyph forms than the Japanese ones (the Chinese ones under a Chinese tag) and other line breaks. Write 'ja': it picks the Japan region (JLReq line breaking and punctuation, sesame emphasis marks, furigana spacing, 図 and 表 labels) and turns hyphenation off. The lint fails a text with kana under a zh or ko tag. Japanese line breaking (kinsoku) →
- A reading cannot stand over a blank yet: a ruby inside
[[…]]is part of the hidden answer. The 漢字の書き取り exercise sets each reading on its own line and the box after it, three to a row in a:::columnsgroup; on the vertical page the reading stands beside the box, as furigana do. - Klee One ships 400 and 600 only:
boldFontWeight: 600keeps every bold run in a cut that exists, the item numbers and the exercise numbers included.
Credits
- Recipe
- Ignacio Ferro
- Text
- Original prose, CC BY 4.0
- Images
- A young tanuki in a yellow school hat holding a big pencil. · Generated With Diffusion Models · original
- A fox cub in a red scarf reading a book. · Generated With Diffusion Models · original
- Fonts
- Klee One (SIL OFL 1.1) · Zen Maru Gothic (SIL OFL 1.1)
Edit this write-up ↗ (opens in a new tab)Recipe folder on GitHub ↗ (opens in a new tab)


