En poques paraules
Un quadern d’exercicis de primària xinès: dues unitats de llengua i de matemàtiques. El mateix text dona l’exemplar de l’alumne, amb buits i solucions al final, i el del mestre, amb cada resposta en vermell al seu lloc.
Què compondràs
Dues unitats d’un 练习册 de segon de primària en format 16开: una de llengua, 春天来了, i una altra de matemàtiques sobre sumes i restes fins a cent. Cada una obre amb una franja d’aquarel·la, una pestanya amb l’assignatura i una línia per a la classe, el nom i la nota, i continua amb els apartats que imprimeix un quadern xinès: 看拼音写词语, lectures per encerclar, 选择题 amb un ( ) després de l’enunciat i A. B. C. D. en fila, 判断题 amb un parèntesi al final de la línia, buits, 连线 entre classificadors i coses, una frase per escriure en 田字格, 口算 en quatre columnes, 竖式 amb una casella per xifra i un problema. Els punts van a cada enunciat, tal com els llegeix l’alumne. El mateix Markdown es compon dos cops: el llibre de l’alumne, amb el 参考答案 al final de cada unitat, i el del mestre, amb cada resposta escrita en vermell, amb lletra Kai, al seu lloc.
Aquesta recepta respon a
- Com componc preguntes d'examen xinès: un parèntesi ( ) després de l'enunciat i les opcions A. B. C. D. en una fila?
- Com faig l'edició del professor, amb les respostes al seu lloc, a partir del llibre de l'alumne?
- Com dono a una pregunta oberta pauta Seyès, quadrícula o quadres 田字格 per escriure?
- Com componc la divisió llarga tal com l'escriuen les escoles de cada país?
La resposta curta
// 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' } });
Ingredients
- Funcions
- Estils d'exerciciEdició del professorElecció i vertader o falsExercicisBuits i bancs de paraulesUnir i classificarPreguntes obertes i zones d'escripturaOperacions en columnaSolucionsNumeració xinesaFonts xineses, japoneses i coreanesRetícula de caràctersPunts d'èmfasi i línies de nom i de títolChips en líniaTres o més columnesObertures dissenyadesAtributs de títolImatges als dissenys de pàginaPaleta de color semànticaExportació a PDF
- També fa servir
- Columnes en un requadre o al textRequadres que es parteixen o noTall de línies en xinèsEquilibri de columnesEstils de paràgrafFonts incrustades al PDFFigures i taules com a recursos
- Tipografia
- Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1)
- Recursos
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, original)
- Two children count oranges, apples and pears at a grandmother's fruit stall, a cat on a stool beside them. (Generated With Diffusion Models, original)
Elaboració
#1 · Un estil per a tots els apartats
El codi és a la resposta curta. Els apartats es compten amb numerals xinesos seguits d’un 、 d’amplada completa, així que després del número no hi va cap espai; l’enunciat va en lletra Hei sense negreta, perquè els punts entre parèntesis es llegeixin com a part seva. options.bracket escriu el ( ) després de cada enunciat, i al llibre del mestre la lletra correcta hi va a dins. Els dos estils petits amplien el principal: pinyin dibuixa cada buit com una caixa tan ampla com la seva paraula i kuohao no dibuixa res, perquè el parèntesi de 在( )里填 ja és al text.
#2 · El llibre de l’alumne i el del mestre amb un sol text
const build = () => withLoadedFonts(() => buildDocument({ markdown, resources }, config()),
{ ...kitFonts(FONTS), text: markdown });
const pupil = await build();
edition = 'teacher';
const docs = [pupil, await build()];
La configuració és una funció, així que cada composició rep el seu propi objecte i el paquet del Sandbox desa el de l’alumne. edition: 'teacher' escriu les respostes amb el color i la lletra d’answers: les paraules a les seves caselles, les lletres als seus parèntesis, √ i × als 判断题, les xifres a les caselles del 竖式 i la frase model amb un caràcter per quadre. El llibre de l’alumne porta les solucions on hi ha la línia :::solutions, al final de cada unitat; el del mestre no les necessita.
#3 · L’obertura d’unitat amb la seva pintura
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' } } },
] } };
La pintura s’ancora a la pàgina, així que arriba a la vora superior tant a la pàgina senar com a la parella; la pestanya i el títol es col·loquen des de l’inici de la caixa de text, sota el peu de la pintura. Les dues unitats comparteixen el disseny i només canvien els seus atributs de títol, kicker i pic.
#4 · Folis al peu
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' }),
] };
Els quaderns xinesos porten el foli al peu. El títol de la sèrie surt de la capçalera YAML del Markdown ({title}), i el de la unitat, del seu títol ({chapterTitle}).
La recepta completa
// ═══ 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') },
L'script.js compost funciona tal com és: enganxa'l com a script de mòdul en qualsevol pàgina o obre la recepta a CodePen. Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)
Variants
#Imprimeix les solucions al final del capítol
Sense línies :::solutions, chapterEnd reuneix les solucions de cada unitat abans del títol de nivell 1 següent.
- solutions: { placement: edition === 'teacher' ? 'none' : 'directive' } });
+ solutions: { placement: edition === 'teacher' ? 'none' : 'chapterEnd' } });#Posa el pinyin sobre els caràcters
La cartilla amb pinyin posa el pinyin sobre els seus caràcters com a ruby, cosa que un buit encara no admet.
Errors freqüents
Error freqüent
Les fonts xineses es carreguen per fragments, amb el bloc cjk
Fontsource serveix una família xinesa, japonesa o coreana en un centenar de fitxers per pes, cadascun amb un interval de caràcters. loadFonts només baixa el fitxer latin, així que a la pantalla els caràcters xinesos surten d'una font del sistema i es mesuren malament, i fontsourceProvider lliura al PDF aquest fitxer latin, que els imprimeix com a caixes buides. Afegeix el bloc cjk del kit, crida loadCjkFonts(FONTS, markdown) després de loadFonts (una vegada per veu, amb el text que compon, si el llibre fa servir diverses fonts xineses) i passa a renderToPdf fontProvider: cjkPdfProvider: tots dos agafen els fitxers que contenen els caràcters del text. Fonts xineses, japoneses i coreanes →
- Una lectura encara no pot anar sobre un buit: un ruby dins de
[[…]]forma part de la resposta oculta, i una zona de 田字格 ocupa tota l’amplada de la línia. L’apartat 看拼音写词语 posa cada síl·laba a la seva pròpia línia i la caixa a sota, quatre per fila en un grup:::columns. - La lletra Kai només compon les respostes, així que carrega únicament els caràcters dels buits i de les respostes model; si compon algun caràcter més, cal afegir-lo a aquest text.
Crèdits
- Recepta
- Ignacio Ferro
- Text
- Text original, CC BY 4.0
- Imatges
- A Chinese village in early spring: willows, swallows, peach blossom and two children on a path between rice fields. · Generated With Diffusion Models · original
- Two children count oranges, apples and pears at a grandmother's fruit stall, a cat on a stool beside them. · Generated With Diffusion Models · original
- Fonts
- Noto Serif SC (SIL OFL 1.1) · Noto Sans SC (SIL OFL 1.1) · LXGW WenKai TC (SIL OFL 1.1)
- Codi
- MIT, com Postext
Editar aquest text ↗ (s'obre en una pestanya nova)Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)


