かんたんな説明
中国の小学校の練習帳です。国語と算数の2単元があります。同じテキストから、空欄があり単元末に解答が付く児童用と、すべての答えを赤字でその場に書いた教師用を作ります。
できあがり
小学2年生向けの练习册から2単元を、16开の判型で組みます。国語の単元「春天来了」と、100までのたし算とひき算を扱う算数の単元です。各単元は水彩画の帯、教科のタブ、クラスと名前と点数の行で始まり、中国の練習帳が刷る大問が続きます。看拼音写词语、丸で囲む読み、問いのあとに( )があり A. B. C. D. を1行に並べる选择题、行末にかっこがある判断题、穴埋め、量詞と物を結ぶ连线、田字格に書く一文、4列の口算、1桁に1マスの竖式、そして文章題です。配点は各問題の指示文の中にあり、児童はそこで読みます。同じ Markdown を2回組みます。児童用は各単元の終わりに参考答案を付け、教師用はすべての答えを赤い楷書体でその場所に書きます。
このレシピが答える質問
- 中国語の選択問題(問の後の( )と一行に並ぶ A. B. C. D.)はどう組む?
- 生徒用から、答えを欄に刷り込んだ教師用を作るには?
- 記述問題の解答欄をセイエス罫、方眼、田字格にするには?
- わり算の筆算を国ごとの学校の書き方で組むには?
手短な答え
// The exercise style is the form; the Markdown holds every word, the points included. The
// same text builds both editions: `edition: 'teacher'` writes each answer into its blank,
// bracket and square in red Kai, rings the right options and draws the matching lines.
const lianxi = {
id: 'lianxi',
number: { format: 'simp-chinese-informal', suffix: '、', fontFamily: HEI, color: col('green') },
instruction: { fontFamily: HEI, bold: false }, // 黑体, not bold: the points stay quiet
// 选择题: A. B. C. D. in a row, a ( ) after the stem for the letter.
options: { labels: 'alpha-upper', suffix: '.', layout: 'row', bracket: true },
trueFalse: { look: 'bracket' }, // 判断题: ( ) at the end of the line, √ or ×
blank: { look: 'line', color: col('ink') },
area: { kind: 'tianzige', color: col('rule') }, // 写话 on 田字格
match: { color: col('green') },
grid: { fontFamily: SONG, rule: col('rule') },
answers: { color: col('red'), fontFamily: KAI },
solutions: { box: 'daan' },
marginTop: em(1.2), marginBottom: em(0.6),
};
const exerciseStyles = [
lianxi,
// 看拼音写词语: a box under each syllable, as wide as its word.
{ id: 'pinyin', extends: 'lianxi', blank: { look: 'box', width: 'answer', color: col('rule') } },
// 在( )里填: the bracket is in the text, the blank inside it draws nothing.
{ id: 'kuohao', extends: 'lianxi', blank: { look: 'none', width: 'answer' } },
];
let edition = 'student'; // the second build below prints the teacher's book
// The pupil's book: 参考答案 where a :::solutions line stands, at the end of each unit.
// The teacher's book: every answer on the page, so no key at the end.
const exercises = () => ({ edition,
solutions: { placement: edition === 'teacher' ? 'none' : 'directive' } });
材料
- 機能
- 練習問題のスタイル教師用指導書選択問題と正誤問題練習問題穴埋めと語群線結びと分類記述問題と解答欄筆算解答中国語の漢数字中国語・日本語・韓国語のフォント文字グリッド中国語の圏点、専名線、書名線インラインのチップ3段以上の段組みデザインした章扉見出しの属性ページデザインの画像セマンティックカラーパレットPDFの書き出し
- 種類
- Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1)
- 素材
market-1600.jpgspring-1600.jpg- A Chinese village in early spring: willows, swallows, peach blossom and two children on a path between rice fields. (Generated With Diffusion Models, オリジナル)
- Two children count oranges, apples and pears at a grandmother's fruit stall, a cat on a stool beside them. (Generated With Diffusion Models, オリジナル)
作り方
#1 · すべての大問をひとつのスタイルで
コードは上の手短な答えにあります。大問は漢数字に全角の「、」を付けて数えるので、番号のあとに語間スペースは入りません。指示文は黒体で、太字にしません。かっこ内の配点が指示文の一部として読めるようにするためです。options.bracket が各問いのあとに( )を書き、教師用では正しい記号がその中に入ります。2つの小さなスタイルはメインのスタイルを拡張します。pinyin は空欄ごとに語と同じ幅の枠を描き、kuohao は何も描きません。「在( )里填」のかっこはテキストの中にあるからです。
#2 · 児童用と教師用をひとつのテキストから
const build = () => withLoadedFonts(() => buildDocument({ markdown, resources }, config()),
{ ...kitFonts(FONTS), text: markdown });
const pupil = await build();
edition = 'teacher';
const docs = [pupil, await build()];
設定は関数なので、組むたびに別のオブジェクトができ、Sandbox のバンドルには児童用の設定が残ります。edition: 'teacher' は answers の色と書体で答えを書きます。語は枠に、記号はかっこに、判断题には √ と × を、竖式のマスには数字を、例文は1マスに1字ずつ書きます。児童用は各単元の終わりにある :::solutions の行に解答を刷ります。教師用には要りません。
#3 · 絵の入った単元の扉
const BAND = 184 / 2.4; // mm: the painting's height at the page's width
const opener = { enabled: true, minHeight: mm(BAND - 22 + 23), slot: { elements: [
{ kind: 'image', id: 'picture', resourceId: '{attr.pic}', decorative: true, reserve: false,
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: mm(184), height: mm(BAND) } } },
{ kind: 'text', id: 'subject', content: '{attr.kicker}', fontFamily: HEI, fontSize: pt(11),
fontWeight: 700, color: col('paper'), letterSpacing: pt(2), align: 'center',
box: { backgroundColor: col('green'), borderRadius: mm(1.5),
padding: { top: mm(0.8), bottom: mm(0.8), left: mm(2.4), right: mm(2) } },
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: mm(0), y: mm(BAND - 22 + 6) } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: HEI, fontSize: pt(24),
fontWeight: 700, color: col('ink'), align: 'left',
placement: { anchor: { to: '#subject', edge: 'right-of' },
offset: { x: mm(4), y: mm(-2.6) } } },
{ kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.2), color: col('green'),
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: mm(0), y: mm(BAND - 22 + 19) }, size: { width: 'fill' } } },
] } };
絵はページに固定されるので、奇数ページでも偶数ページでも上端まで届きます。教科のタブと題は版面の上端から配置し、絵の下端より下に来ます。2つの単元は同じデザインを使い、違うのは見出しの属性 kicker と pic だけです。
#4 · ノンブルは下に
const foot = (id, content, parity, edge, x, extra = {}) => ({ kind: 'text', id, content,
parity, fontFamily: HEI, fontSize: pt(8), color: col('muted'),
placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(-10) } }, ...extra });
const footer = { elements: [
foot('folio-v', '{pageNumber}', 'even', 'bottom-left', 16,
{ fontWeight: 700, color: col('green') }),
foot('series', '{title}', 'even', 'bottom-left', 24),
foot('unit', '{chapterTitle}', 'odd', 'bottom-right', -24, { align: 'right' }),
foot('folio-r', '{pageNumber}', 'odd', 'bottom-right', -16,
{ fontWeight: 700, color: col('green'), align: 'right' }),
] };
中国の練習帳はノンブルをページの下に置きます。シリーズ名は Markdown のフロントマター({title})から、単元名は見出し({chapterTitle})から取ります。
レシピの全体
// ═══ Postext Cookbook · Nº 171 · A Chinese exercise book, pupil's and teacher's ═══
// https://postext.dev/en/cookbook/chinese-exercise-book
// Code: MIT · Text: original (CC BY 4.0) · Pictures: generated with diffusion models
// Fonts: Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1) · Needs postext ≥ 1.59.0
import {
buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
const LANG = 'zh'; // @lang: the language of the frame; the exercise book is Chinese in all
const RECIPE = 'chinese-exercise-book';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a green exercise book, and the red of the teacher's pen
const palette = {
ink: '#1f2523', // text: a green-tinted near-black
green: '#2c7350', // the one accent: section numbers, the subject tab, the squares
red: '#d0352c', // the teacher's answers
tint: '#e6f2ea', // the subject tab's ground, the answers box
rule: '#8fb9a2', // 田字格, the matching dots' lines
muted: '#68736d', // folios, the series line
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.green, model: 'hex' } },
];
// #endregion
// 宋 for the text, 黑 for the instructions and labels, 楷 for what the teacher writes.
const [SONG, HEI, KAI] = ['Noto Serif SC', 'Noto Sans SC', 'LXGW WenKai TC'];
const CHARS = 40; // characters per line: 40 × 10.5 pt = 148 mm
const SIZE = 10.5; // pt: 五号, the size of a school book's text
// #region answer: sections 一、二、 that print the pupil's book or the teacher's
// The exercise style is the form; the Markdown holds every word, the points included. The
// same text builds both editions: `edition: 'teacher'` writes each answer into its blank,
// bracket and square in red Kai, rings the right options and draws the matching lines.
const lianxi = {
id: 'lianxi',
number: { format: 'simp-chinese-informal', suffix: '、', fontFamily: HEI, color: col('green') },
instruction: { fontFamily: HEI, bold: false }, // 黑体, not bold: the points stay quiet
// 选择题: A. B. C. D. in a row, a ( ) after the stem for the letter.
options: { labels: 'alpha-upper', suffix: '.', layout: 'row', bracket: true },
trueFalse: { look: 'bracket' }, // 判断题: ( ) at the end of the line, √ or ×
blank: { look: 'line', color: col('ink') },
area: { kind: 'tianzige', color: col('rule') }, // 写话 on 田字格
match: { color: col('green') },
組み立てたscript.jsはそのまま動きます。任意のページのモジュールスクリプトに貼り付けるか、レシピをCodePenで開いてください。 GitHub上のレシピのフォルダー ↗ (新しいタブで開きます)
アレンジ
よくあるつまずき
つまずき
中国語の書体はcjkブロックを通じてスライス単位で読み込む
Fontsourceは中国語、日本語、韓国語のファミリーを、ウェイトごとに約100個のファイルとして配信し、各ファイルが文字の範囲を受け持ちます。loadFontsが取得するのはlatinファイルだけなので、画面では漢字がシステムの書体で出て計測が狂い、fontsourceProviderはPDFにそのlatinファイルを渡すため、漢字が空の四角で印字されます。キットのcjkブロックを挙げ、loadFontsのあとにloadCjkFonts(FONTS, markdown)を呼び(本で複数のCJK書体を使うときは、それぞれの書体で組むテキストを渡して書体ごとに1回)、renderToPdfにfontProvider: cjkPdfProviderを渡してください。どちらもテキストの文字を含むファイルを取得します。 中国語・日本語・韓国語のフォント →
- 空欄の上にはまだ読みを置けません。
[[…]]の中のルビは隠れた答えの一部になり、田字格の領域は行の幅いっぱいを占めます。看拼音写词语の大問では、音節を1行ずつ置いてその下に枠を置き、:::columnsのグループで1行に4つ並べます。 - 楷書体は答えだけを組むので、空欄と模範解答の字だけを読み込みます。ほかの場所でこの書体を使う字があれば、そのテキストに加えてください。
クレジット
- 本文
- 書き下ろしの文章, CC BY 4.0
- 画像
- A Chinese village in early spring: willows, swallows, peach blossom and two children on a path between rice fields. · Generated With Diffusion Models · オリジナル
- Two children count oranges, apples and pears at a grandmother's fruit stall, a cat on a stool beside them. · Generated With Diffusion Models · オリジナル
- フォント
- Noto Serif SC (SIL OFL 1.1) · Noto Sans SC (SIL OFL 1.1) · LXGW WenKai TC (SIL OFL 1.1)


