En pocas palabras
Un cuaderno de ejercicios de primaria chino: dos unidades de lengua y de matemáticas. El mismo texto da el ejemplar del alumno, con huecos y soluciones al final, y el del profesor, con cada respuesta en rojo en su sitio.
Lo que vas a componer
Dos unidades de un 练习册 de segundo de primaria en formato 16开: una de lengua, 春天来了, y otra de matemáticas sobre sumas y restas hasta cien. Cada una abre con una franja de acuarela, una pestaña con la asignatura y una línea para la clase, el nombre y la nota, y sigue con los apartados que imprime un cuaderno chino: 看拼音写词语, lecturas que rodear, 选择题 con un ( ) tras el enunciado y A. B. C. D. en fila, 判断题 con un paréntesis al final de la línea, huecos, 连线 entre clasificadores y cosas, una frase que escribir en 田字格, 口算 en cuatro columnas, 竖式 con una casilla por cifra y un problema. Los puntos van en cada enunciado, como los lee el alumno. El mismo Markdown se compone dos veces: el libro del alumno, con el 参考答案 al final de cada unidad, y el del profesor, con cada respuesta escrita en rojo, en letra Kai, en su sitio.
Esta receta responde a
- ¿Cómo compongo preguntas de examen chino: un paréntesis ( ) tras el enunciado y las opciones A. B. C. D. en una fila?
- ¿Cómo saco la edición del profesor, con las respuestas impresas en su sitio, del mismo libro del alumno?
- ¿Cómo doy a una pregunta abierta pauta Seyès francesa, cuadrícula o cuadros 田字格 para escribir?
- ¿Cómo compongo la división larga tal como la escriben las escuelas de cada país?
La respuesta corta
// 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' } });
Ingredientes
- Funciones
- Estilos de ejercicioEdición del profesorElección y verdadero o falsoEjerciciosHuecos y bancos de palabrasUnir y clasificarPreguntas abiertas y zonas de escrituraOperaciones en columnaSolucionesNumeración chinaFuentes chinas, japonesas y coreanasRetícula de caracteresPuntos de énfasis y líneas de nombre y de títuloChips en líneaTres o más columnasAperturas diseñadasAtributos de títuloImágenes en los diseños de páginaPaleta de color semánticaExportación a PDF
- También usa
- Columnas en un recuadro o en el textoRecuadros que se parten o noCorte de líneas en chinoEquilibrado de columnasEstilos de párrafoFuentes incrustadas en el PDFFiguras y tablas como recursos
- Tipografía
- Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1)
- Recursos
market-1600.jpgspring-1600.jpg- Un pueblo chino a comienzos de la primavera: sauces, golondrinas, flor de melocotonero y dos niños en un camino entre arrozales. (Generated With Diffusion Models, original)
- Dos niños cuentan naranjas, manzanas y peras en el puesto de fruta de una abuela, con un gato en un taburete. (Generated With Diffusion Models, original)
Elaboración
#1 · Un estilo para todos los apartados
El código está en la respuesta corta. Los apartados se cuentan con numerales chinos seguidos de un 、 de ancho completo, así que tras el número no va ningún espacio; el enunciado va en la letra Hei sin negrita, para que los puntos entre paréntesis se lean como parte de él. options.bracket escribe el ( ) tras cada enunciado, y en el libro del profesor la letra correcta entra dentro. Los dos estilos pequeños amplían el principal: pinyin dibuja cada hueco como una caja tan ancha como su palabra y kuohao no dibuja nada, porque el paréntesis de 在( )里填 ya está en el texto.
#2 · El libro del alumno y el del profesor con un solo texto
const build = () => withLoadedFonts(() => buildDocument({ markdown, resources }, config()),
{ ...kitFonts(FONTS), text: markdown });
const pupil = await build();
edition = 'teacher';
const docs = [pupil, await build()];
La configuración es una función, así que cada composición recibe su propio objeto y el paquete del Sandbox guarda el del alumno. edition: 'teacher' escribe las respuestas con el color y la letra de answers: las palabras en sus casillas, las letras en sus paréntesis, √ y × en los 判断题, las cifras en las casillas del 竖式 y la frase modelo con un carácter por cuadro. El libro del alumno lleva las soluciones donde está la línea :::solutions, al final de cada unidad; el del profesor no las necesita.
#3 · La apertura de unidad con su 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 se ancla a la página, así que llega al borde superior tanto en la página impar como en la par; la pestaña y el título se colocan desde el arranque de la caja de texto, bajo el pie de la pintura. Las dos unidades comparten el diseño y solo cambian sus atributos de título, kicker y pic.
#4 · Folios al pie
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' }),
] };
Los cuadernos chinos llevan el folio al pie. El título de la serie sale de la cabecera YAML del Markdown ({title}), y el de la unidad, de su título ({chapterTitle}).
La receta 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') },
El script.js compuesto funciona tal cual: pégalo como script de módulo en cualquier página o abre la receta en CodePen. Carpeta de la receta en GitHub ↗ (se abre en una nueva pestaña)
Variantes
#Imprime las soluciones al final del capítulo
Sin líneas :::solutions, chapterEnd reúne las soluciones de cada unidad antes del siguiente título de nivel 1.
- solutions: { placement: edition === 'teacher' ? 'none' : 'directive' } });
+ solutions: { placement: edition === 'teacher' ? 'none' : 'chapterEnd' } });#Pon el pinyin sobre los caracteres
La cartilla de pinyin pone el pinyin sobre sus caracteres como ruby, algo que un hueco todavía no admite.
Errores frecuentes
Error frecuente
Las fuentes chinas se cargan por fragmentos, con el bloque cjk
Fontsource sirve una familia china, japonesa o coreana en un centenar de archivos por peso, cada uno con un intervalo de caracteres. loadFonts solo descarga el archivo latin, así que en pantalla los caracteres chinos salen de una fuente del sistema y se miden mal, y fontsourceProvider entrega al PDF ese archivo latin, que los imprime como cajas vacías. Añade el bloque cjk del kit, llama a loadCjkFonts(FONTS, markdown) después de loadFonts (una vez por voz, con el texto que compone, si el libro usa varias fuentes chinas) y pasa a renderToPdf fontProvider: cjkPdfProvider: ambos toman los archivos que contienen los caracteres del texto. Fuentes chinas, japonesas y coreanas →
- Una lectura todavía no puede ir sobre un hueco: un ruby dentro de
[[…]]forma parte de la respuesta oculta, y una zona de 田字格 ocupa toda la medida. El apartado 看拼音写词语 pone cada sílaba en su propia línea y la caja debajo, cuatro por fila en un grupo:::columns. - La letra Kai solo compone las respuestas, así que carga únicamente los caracteres de los huecos y de las respuestas modelo; si compone algún carácter más, hay que añadirlo a ese texto.
Créditos
- Receta
- Ignacio Ferro
- Texto
- Texto original, CC BY 4.0
- Imágenes
- Un pueblo chino a comienzos de la primavera: sauces, golondrinas, flor de melocotonero y dos niños en un camino entre arrozales. · Generated With Diffusion Models · original
- Dos niños cuentan naranjas, manzanas y peras en el puesto de fruta de una abuela, con un gato en un taburete. · Generated With Diffusion Models · original
- Fuentes
- Noto Serif SC (SIL OFL 1.1) · Noto Sans SC (SIL OFL 1.1) · LXGW WenKai TC (SIL OFL 1.1)
- Código
- MIT, como Postext
Editar este texto ↗ (se abre en una nueva pestaña)Carpeta de la receta en GitHub ↗ (se abre en una nueva pestaña)


