Please create a colorful handbook pdf from attached file in that please write individual answers for every individual question on page 54 under' speaking" title . Also explain topic " defending and justifying " under topic Developing conversations.Explain in a very easy and creative logical way with many examples to understand in better way , later also explain grammar topic " Ellipsis and substitution " in a very easy and creative logical way with many more examples. Also add Russian translation under every sentences.

Reading File
Running Command

cd "/tmp/workspace/attachments/12ed78d2-70b7-46f2-a682-f1c26a8bfac2/" && ls -la && python3 -c " import pypdf r=pypdf.PdfReader('DOC-20261108-WA0019 (1).pdf') print(len(r.pages)) for i,p in enumerate(r.pages): t=p.extract_text() or '' print(i+1,len(t),t[:150].replace('\n',' | ')) "

Running Command

cd "/tmp/workspace/attachments/12ed78d2-70b7-46f2-a682-f1c26a8bfac2/" && python3 -c " import pypdf r=pypdf.PdfReader('DOC-20261108-WA0019 (1).pdf') for i,p in enumerate(r.pages): print('=====PAGE',i+1); print(p.extract_text()) "

Running Command

cd /tmp/workspace && mkdir -p handbook/assets handbook/output && python3 -c " import pypdf r=pypdf.PdfReader('attachments/12ed78d2-70b7-46f2-a682-f1c26a8bfac2/DOC-20261108-WA0019 (1).pdf') for i in (0,1): for j,im in enumerate(r.pages[i].images): fn=f'handbook/assets/p{i+1}_{j}_'+im.name open(fn,'wb').write(im.data); print(fn,len(im.data)) "; fc-list | grep -iE "dejavu|noto|liberation|roboto|open" | head -20; python3 -c "import matplotlib;print(matplotlib.get_data_path())"

Loading Image
Running Command

cd /tmp/workspace/handbook/assets && python3 -c " from PIL import Image for f in ['p1_0_Im0.jpg','p2_0_Im0.jpg']: im=Image.open(f); print(f,im.size); im.thumbnail((900,900)); im.save('s_'+f) "; ls /usr/share/fonts/*/ | head -50; ls /opt/amazon/genesis1p-tools/venv/lib64/python3.12/site-packages/matplotlib/mpl-data/fonts/ttf | grep -i dejavu

Reading File
Writing File

~/handbook/src/lib.py

from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import (Paragraph, Spacer, Table, TableStyle, KeepTogether,
                                PageBreak, Image, NextPageTemplate, CondPageBreak)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

FD = '/opt/amazon/genesis1p-tools/venv/lib64/python3.12/site-packages/matplotlib/mpl-data/fonts/ttf/'
pdfmetrics.registerFont(TTFont('DV', FD + 'DejaVuSans.ttf'))
pdfmetrics.registerFont(TTFont('DV-B', FD + 'DejaVuSans-Bold.ttf'))
pdfmetrics.registerFont(TTFont('DV-I', FD + 'DejaVuSans-Oblique.ttf'))
pdfmetrics.registerFont(TTFont('DV-BI', FD + 'DejaVuSans-BoldOblique.ttf'))
pdfmetrics.registerFontFamily('DV', normal='DV', bold='DV-B', italic='DV-I', boldItalic='DV-BI')

PW, PH = A4
MARGIN = 1.7 * cm
W = PW - 2 * MARGIN

def C(h):
    return colors.HexColor(h)

# palette
CORAL = '#E8505B'; CORAL_L = '#FDECEE'
ORANGE = '#F39C12'; ORANGE_L = '#FEF5E3'
TEAL = '#16A085'; TEAL_L = '#E3F6F1'
BLUE = '#2E86DE'; BLUE_L = '#E6F1FC'
PURPLE = '#8E44AD'; PURPLE_L = '#F3E8F8'
NAVY = '#1B2A49'
YELLOW_L = '#FFF8D6'
GREEN = '#27AE60'; GREEN_L = '#E5F7EC'
GREY = '#555B6E'

EN = ParagraphStyle('EN', fontName='DV', fontSize=9.6, leading=13.4, textColor=C('#1d1d2b'))
RU = ParagraphStyle('RU', fontName='DV-I', fontSize=8.7, leading=12, textColor=C('#3a6ea5'),
                    spaceAfter=4.5)
EN_S = ParagraphStyle('EN_S', parent=EN, fontSize=8.8, leading=12.2)
RU_S = ParagraphStyle('RU_S', parent=RU, fontSize=8.0, leading=11, spaceAfter=3)
BODY_C = ParagraphStyle('BC', parent=EN, alignment=TA_CENTER)
H_WHITE = ParagraphStyle('HW', fontName='DV-B', fontSize=19, leading=24, textColor=colors.white)
H_WHITE_S = ParagraphStyle('HWS', fontName='DV-I', fontSize=9.5, leading=13, textColor=colors.white)
SUB = ParagraphStyle('SUB', fontName='DV-B', fontSize=12, leading=16, textColor=C(NAVY))
SUB_RU = ParagraphStyle('SUBRU', fontName='DV-I', fontSize=9, leading=12, textColor=C('#3a6ea5'), spaceAfter=4)


def esc(t):
    return t


def ep(t, st=EN):
    return Paragraph(t, st)


def pair(e, r, small=False, color=None):
    """English paragraph followed by Russian translation."""
    s1, s2 = (EN_S, RU_S) if small else (EN, RU)
    if color:
        s1 = ParagraphStyle('x', parent=s1, textColor=C(color))
    return [Paragraph(e, s1), Paragraph(r, s2)]


def sp(h=6):
    return Spacer(1, h)


def banner(title, subtitle_en, subtitle_ru, color, color2=None, height=None):
    inner = [Paragraph(title, H_WHITE), Spacer(1, 2),
             Paragraph(subtitle_en, H_WHITE_S), Paragraph(subtitle_ru, H_WHITE_S)]
    t = Table([[inner]], colWidths=[W])
    t.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, -1), C(color)),
        ('ROUNDEDCORNERS', [12, 12, 12, 12]),
        ('LEFTPADDING', (0, 0), (-1, -1), 14), ('RIGHTPADDING', (0, 0), (-1, -1), 14),
        ('TOPPADDING', (0, 0), (-1, -1), 10), ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
    ]))
    return t


def box(flows, bg, border=None, pad=9, width=W):
    t = Table([[flows]], colWidths=[width])
    st = [('BACKGROUND', (0, 0), (-1, -1), C(bg)),
          ('ROUNDEDCORNERS', [9, 9, 9, 9]),
          ('LEFTPADDING', (0, 0), (-1, -1), pad), ('RIGHTPADDING', (0, 0), (-1, -1), pad),
          ('TOPPADDING', (0, 0), (-1, -1), pad - 2), ('BOTTOMPADDING', (0, 0), (-1, -1), pad - 3)]
    if border:
        st.append(('BOX', (0, 0), (-1, -1), 1.3, C(border)))
    t.setStyle(TableStyle(st))
    return t


def pill(text, color, width=None, txt='#ffffff', size=10):
    st = ParagraphStyle('pill', fontName='DV-B', fontSize=size, leading=size + 3, textColor=C(txt))
    t = Table([[Paragraph(text, st)]], colWidths=[width or W])
    t.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, -1), C(color)),
        ('ROUNDEDCORNERS', [8, 8, 8, 8]),
        ('LEFTPADDING', (0, 0), (-1, -1), 9), ('RIGHTPADDING', (0, 0), (-1, -1), 9),
        ('TOPPADDING', (0, 0), (-1, -1), 4), ('BOTTOMPADDING', (0, 0), (-1, -1), 5)]))
    return t


def question_head(num, en_q, ru_q, color, light):
    n = Paragraph('<font color="white"><b>%s</b></font>' % num,
                  ParagraphStyle('n', fontName='DV-B', fontSize=11, alignment=TA_CENTER, leading=14))
    q = [Paragraph('<b>%s</b>' % en_q, ParagraphStyle('q', parent=EN, textColor=C(NAVY))),
         Paragraph(ru_q, RU_S)]
    t = Table([[n, q]], colWidths=[1.0 * cm, W - 1.0 * cm])
    t.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (0, 0), C(color)),
        ('BACKGROUND', (1, 0), (1, 0), C(light)),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('LEFTPADDING', (1, 0), (1, 0), 8),
        ('TOPPADDING', (0, 0), (-1, -1), 5), ('BOTTOMPADDING', (0, 0), (-1, -1), 2),
        ('ROUNDEDCORNERS', [8, 8, 8, 8])]))
    return t


def bullets(items, color, small=False, width=W):
    """items = [(en, ru)]; returns a table with coloured dot bullets."""
    rows = []
    for e, r in items:
        rows.append([Paragraph('<font color="%s"><b>●</b></font>' % color,
                               ParagraphStyle('b', fontName='DV', fontSize=7, leading=13, alignment=TA_CENTER)),
                     pair(e, r, small)])
    t = Table(rows, colWidths=[0.5 * cm, width - 0.5 * cm])
    t.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'),
                           ('LEFTPADDING', (0, 0), (-1, -1), 1), ('RIGHTPADDING', (0, 0), (-1, -1), 1),
                           ('TOPPADDING', (0, 0), (-1, -1), 1), ('BOTTOMPADDING', (0, 0), (-1, -1), 0)]))
    return t


def dialogue(rows, ca, cb, la, lb, width=W):
    """rows = [(speaker 'A'/'B', en, ru)]"""
    data = []
    sty = []
    for i, (s, e, r) in enumerate(rows):
        col, light = (ca, la) if s == 'A' else (cb, lb)
        lab = Paragraph('<font color="white"><b>%s</b></font>' % s,
                        ParagraphStyle('l', fontName='DV-B', fontSize=10, alignment=TA_CENTER, leading=13))
        data.append([lab, pair(e, r, small=True)])
        sty += [('BACKGROUND', (0, i), (0, i), C(col)), ('BACKGROUND', (1, i), (1, i), C(light))]
    t = Table(data, colWidths=[0.7 * cm, width - 0.7 * cm])
    t.setStyle(TableStyle(sty + [
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('LEFTPADDING', (1, 0), (1, -1), 8), ('LEFTPADDING', (0, 0), (0, -1), 0), ('RIGHTPADDING', (0, 0), (0, -1), 0),
        ('TOPPADDING', (0, 0), (-1, -1), 3), ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
        ('LINEBELOW', (0, 0), (-1, -2), 1.5, colors.white),
        ('ROUNDEDCORNERS', [7, 7, 7, 7])]))
    return t


def grid(rows, widths, head_color, light_rows=('#ffffff', '#f6f6fb'), head=True):
    """rows: list of lists of flowable/str cells"""
    data = []
    for ri, row in enumerate(rows):
        out = []
        for c in row:
            if isinstance(c, str):
                if ri == 0 and head:
                    out.append(Paragraph('<font color="white"><b>%s</b></font>' % c,
                                         ParagraphStyle('h', fontName='DV-B', fontSize=8.8, leading=11.5)))
                else:
                    out.append(Paragraph(c, EN_S))
            else:
                out.append(c)
        data.append(out)
    t = Table(data, colWidths=widths, repeatRows=1 if head else 0)
    st = [('VALIGN', (0, 0), (-1, -1), 'TOP'),
          ('LEFTPADDING', (0, 0), (-1, -1), 6), ('RIGHTPADDING', (0, 0), (-1, -1), 6),
          ('TOPPADDING', (0, 0), (-1, -1), 4), ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
          ('LINEBELOW', (0, 0), (-1, -1), 0.4, C('#d8d8e4')),
          ('ROUNDEDCORNERS', [6, 6, 6, 6])]
    if head:
        st.append(('BACKGROUND', (0, 0), (-1, 0), C(head_color)))
    for i in range(1 if head else 0, len(rows)):
        st.append(('BACKGROUND', (0, i), (-1, i), C(light_rows[i % 2])))
    t.setStyle(TableStyle(st))
    return t


def ph(en, ru, color):
    """phrase highlight line (single coloured sentence + translation)"""
    return [Paragraph('<font color="%s"><b>%s</b></font>' % (color, en), EN), Paragraph(ru, RU)]
Writing File

~/handbook/src/part1.py

from lib import *

def build():
    s = []
    s.append(banner('PART 1 · SPEAKING (page 54)', 'Model answers for every question: Exercises 1, 2 and 3. Change the details to match your own opinion!',
                    'Образцы ответов на каждый вопрос: упражнения 1, 2 и 3. Меняйте детали под своё мнение!', CORAL))
    s.append(sp(7))
    s.append(box([
        Paragraph('<b>Opinion toolkit / Фразы для выражения мнения</b>', ParagraphStyle('t', parent=EN, textColor=C(CORAL))),
        *pair('In my opinion... / I would say... / It seems to me that... / I strongly agree because... / I only partly agree, since...',
              'По моему мнению... / Я бы сказал(а)... / Мне кажется, что... / Я полностью согласен(на), потому что... / Я согласен(на) лишь частично, так как...', True),
        *pair('<b>Word bank:</b> let off steam = to release anger; row = a noisy quarrel; clear the air = to remove tension; lose your temper = to get very angry; in-laws = your husband\'s / wife\'s family.',
              '<b>Слова:</b> let off steam = выпустить пар; row = громкая ссора; clear the air = разрядить обстановку; lose your temper = потерять самообладание; in-laws = родственники мужа / жены.', True)],
        CORAL_L, CORAL))
    s.append(sp(9))

    # ---------- Exercise 1 ----------
    s.append(pill('EXERCISE 1 · Work in pairs. Discuss the questions.  /  Упражнение 1. Обсудите вопросы в паре.', CORAL))
    s.append(sp(7))

    # Q1
    s.append(question_head('1', 'What do you think the relationship is between the people in the photo? Why?',
                           'Как вы думаете, какие отношения между людьми на фото? Почему?', CORAL, CORAL_L))
    s.append(sp(4))
    s.append(bullets([
        ('<b>My answer:</b> I think they are not close friends. They are probably strangers or casual acquaintances who are having a heated discussion in a public place.',
         '<b>Мой ответ:</b> Думаю, они не близкие друзья. Скорее всего, это незнакомые люди или просто знакомые, которые горячо спорят в общественном месте.'),
        ('<b>Reason 1:</b> The man on the right is pointing his finger straight at the man in the middle. People usually do this to an opponent, not to a close friend.',
         '<b>Причина 1:</b> Мужчина справа указывает пальцем прямо на мужчину в центре. Так обычно ведут себя с оппонентом, а не с близким другом.'),
        ('<b>Reason 2:</b> The man in the blue jacket has his arms crossed and looks calm, even a little amused. He seems to be a spectator who is just watching.',
         '<b>Причина 2:</b> Мужчина в синем пиджаке скрестил руки и выглядит спокойным, даже слегка позабавленным. Похоже, он просто наблюдает со стороны.'),
        ('<b>Reason 3:</b> There is a crowd in the background, and the caption says Parliament Square. It is a famous place for protests and public debates, so people there often argue with strangers.',
         '<b>Причина 3:</b> На заднем плане толпа, а подпись говорит, что это Парламентская площадь. Это известное место для протестов и публичных дискуссий, поэтому там часто спорят с незнакомыми людьми.'),
        ('I could be wrong, though. They might be colleagues or old friends who simply enjoy a loud debate.',
         'Впрочем, я могу ошибаться. Это могут быть коллеги или старые друзья, которым просто нравятся громкие споры.'),
    ], CORAL))
    s.append(sp(9))

    # Q2
    s.append(question_head('2', 'What do you think they might be arguing about? What might they be saying?',
                           'Как вы думаете, о чём они могут спорить? Что они могут говорить?', CORAL, CORAL_L))
    s.append(sp(4))
    s.append(bullets([
        ('<b>My answer:</b> I think they are arguing about politics or a social issue, for example a new law or a government decision.',
         '<b>Мой ответ:</b> Думаю, они спорят о политике или об общественной проблеме, например о новом законе или решении правительства.'),
        ('The photo was taken near the Houses of Parliament, where people gather to protest and to share their views, so a political topic is the most logical guess.',
         'Фото сделано возле здания Парламента, где люди собираются на протесты и делятся мнениями, поэтому политическая тема - самое логичное предположение.'),
    ], CORAL))
    s.append(sp(4))
    s.append(ep('<b>What they might be saying / Что они могут говорить:</b>', ParagraphStyle('t', parent=EN, textColor=C(CORAL))))
    s.append(sp(2))
    s.append(dialogue([
        ('A', '<b>Man in white (pointing):</b> "You can\'t just ignore the facts!"', '<b>Мужчина в белом (показывает пальцем):</b> «Вы не можете просто игнорировать факты!»'),
        ('A', '"Listen to me for a second! That\'s simply not true, and you know it."', '«Послушайте меня секунду! Это просто неправда, и вы это знаете.»'),
        ('B', '<b>Man with headphones:</b> "Hang on, that\'s not what I said."', '<b>Мужчина в наушниках:</b> «Подождите, я не это говорил.»'),
        ('B', '"I take your point, but you\'re missing the bigger picture."', '«Я понимаю вашу мысль, но вы упускаете общую картину.»'),
        ('A', '<b>Man in blue jacket:</b> "Calm down, guys. It\'s not worth getting so worked up."', '<b>Мужчина в синем пиджаке:</b> «Успокойтесь, ребята. Не стоит так заводиться.»'),
        ('A', '"Let\'s hear each other out before we judge."', '«Давайте сначала выслушаем друг друга, а потом будем судить.»'),
    ], CORAL, BLUE, CORAL_L, BLUE_L))
    s.append(sp(9))

    # Q3
    s.append(CondPageBreak(6 * cm))
    s.append(question_head('3', 'Which of these sentences about arguing do you agree with? Why?',
                           'С какими из этих высказываний об ссорах вы согласны? Почему?', CORAL, CORAL_L))
    s.append(sp(5))
    stm = [
        ('1', 'It\'s healthy to let off steam every once in a while.', 'Время от времени полезно выпускать пар.',
         'I AGREE', GREEN,
         'Keeping anger inside is like closing the lid on a boiling pot: sooner or later it explodes. If we let off steam in a safe way, for example in a short honest talk, we feel lighter.',
         'Держать злость в себе - как закрыть крышку на кипящей кастрюле: рано или поздно она взорвётся. Если выпускать пар безопасно, например в коротком честном разговоре, становится легче.',
         'After a hard week I told my sister I was fed up with the noise at home. We both felt better afterwards.',
         'После тяжёлой недели я сказал(а) сестре, что мне надоел шум дома. После этого нам обеим стало легче.'),
        ('2', 'As soon as you lose your temper, you lose the argument.', 'Как только вы теряете самообладание, вы проигрываете спор.',
         'MOSTLY AGREE', GREEN,
         'When we are furious, we say things we do not mean and the other person stops listening to our point. However, staying calm does not automatically make you right.',
         'Когда мы в ярости, мы говорим то, чего не имеем в виду, и собеседник перестаёт слушать нашу мысль. Но спокойствие ещё не делает вас правым.',
         'In a calm debate, the quiet person is usually the one people believe.',
         'В спокойном споре обычно верят тому, кто говорит тихо.'),
        ('3', 'Sometimes people need a good row to clear the air.', 'Иногда людям нужна хорошая ссора, чтобы разрядить обстановку.',
         'I AGREE', GREEN,
         'When tension grows silently, an honest quarrel can finally show the real problem. But the row should end with an apology or a hug, not with new insults.',
         'Когда напряжение растёт молча, честная ссора может наконец показать настоящую проблему. Но ссора должна закончиться извинением или объятием, а не новыми оскорблениями.',
         'Two colleagues who shouted at each other about deadlines later became a great team.',
         'Двое коллег, накричавших друг на друга из-за сроков, позже стали отличной командой.'),
        ('4', 'Arguing can become addictive and can have a terrible impact on relationships.', 'Ссоры могут стать привычкой (вызывать зависимость) и ужасно влиять на отношения.',
         'PARTLY AGREE', ORANGE,
         'Some couples get used to drama and start fighting about everything. Constant fighting destroys trust. But not all arguing is bad; the dangerous thing is the habit.',
         'Некоторые пары привыкают к драмам и начинают ссориться из-за всего. Постоянные ссоры разрушают доверие. Но не всякий спор плох; опасна именно привычка.',
         'A friend of mine argued every evening, and in the end the relationship simply ended.',
         'Мой друг ссорился каждый вечер, и в конце концов отношения просто закончились.'),
        ('5', 'Raising your voice is a form of aggression.', 'Повышать голос - это форма агрессии.',
         'PARTLY DISAGREE', CORAL,
         'It depends on the intention and the culture. In some families people talk loudly and with passion and nobody is hurt. Still, if your voice frightens the other person, it is aggressive.',
         'Это зависит от намерения и культуры. В некоторых семьях говорят громко и страстно, и никто не обижается. Однако если ваш голос пугает собеседника, это агрессия.',
         'My Italian neighbours sound angry all the time, but they are the friendliest people I know.',
         'Мои итальянские соседи всё время звучат сердито, но они самые дружелюбные люди, каких я знаю.'),
        ('6', 'An argument may be unpleasant, but it\'s often the first step towards a solution.', 'Ссора может быть неприятной, но часто это первый шаг к решению.',
         'I STRONGLY AGREE', GREEN,
         'You cannot fix a problem that nobody talks about. An argument puts the problem on the table.',
         'Нельзя решить проблему, о которой никто не говорит. Ссора выкладывает проблему на стол.',
         'Roommates who argued about dirty dishes finally made a cleaning schedule.',
         'Соседи по комнате, поспорившие из-за грязной посуды, наконец составили график уборки.'),
    ]
    for n, e, r, verdict, vc, why_e, why_r, ex_e, ex_r in stm:
        head = Table([[Paragraph('<b>%s. "%s"</b>' % (n, e), ParagraphStyle('s', parent=EN_S, textColor=C(NAVY))),
                       Paragraph('<font color="white"><b>%s</b></font>' % verdict,
                                 ParagraphStyle('v', fontName='DV-B', fontSize=7.5, leading=10, alignment=TA_CENTER))]],
                     colWidths=[W - 3.3 * cm, 3.1 * cm])
        head.setStyle(TableStyle([('BACKGROUND', (1, 0), (1, 0), C(vc)), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                                  ('ROUNDEDCORNERS', [6, 6, 6, 6]), ('TOPPADDING', (0, 0), (-1, -1), 3),
                                  ('BOTTOMPADDING', (0, 0), (-1, -1), 3)]))
        s.append(KeepTogether([
            head, Paragraph(r, RU_S),
            *pair('<b>Why:</b> ' + why_e, '<b>Почему:</b> ' + why_r, True),
            *pair('<b>Example:</b> ' + ex_e, '<b>Пример:</b> ' + ex_r, True), sp(6)]))

    # ---------- Exercise 2 ----------
    s.append(PageBreak())
    s.append(pill('EXERCISE 2 · New partner. How can each topic lead to arguments? What might be said?  /  Как каждая тема приводит к спорам? Что могут сказать?', ORANGE))
    s.append(sp(6))
    topics = [
        ('careers', 'карьера',
         'Partners or parents disagree about job choices, promotions, or moving to another city for work.',
         'Партнёры или родители не согласны по поводу выбора работы, повышения или переезда в другой город.',
         '"You\'re giving up a great job just because it\'s far away?"', '«Ты отказываешься от отличной работы только потому, что она далеко?»',
         '"It\'s my career, and I\'ll decide what\'s best for it!"', '«Это моя карьера, и я сам решу, что для неё лучше!»'),
        ('politics', 'политика',
         'People hold strong opinions, and each side feels the other is not only wrong but also unreasonable.',
         'У людей сильные взгляды, и каждая сторона считает другую не просто неправой, но и неразумной.',
         '"How can you vote for them?"', '«Как ты можешь за них голосовать?»',
         '"You never listen to the facts!"', '«Ты никогда не слушаешь факты!»'),
        ('exes', 'бывшие партнёры',
         'Jealousy and insecurity appear when a partner still talks to, or compares you with, an ex.',
         'Ревность и неуверенность появляются, когда партнёр всё ещё общается с бывшим или сравнивает вас с ним.',
         '"Why are you still following her on social media?"', '«Почему ты до сих пор подписан на неё в соцсетях?»',
         '"She\'s just a friend now. Stop being so jealous!"', '«Теперь она просто друг. Хватит ревновать!»'),
        ('religion', 'религия',
         'Beliefs are very personal, so criticism feels like an attack on who you are. Arguments also start about holidays and how to raise children.',
         'Убеждения очень личные, поэтому критика воспринимается как нападение на личность. Споры также возникают из-за праздников и воспитания детей.',
         '"You don\'t respect my beliefs!"', '«Ты не уважаешь мои убеждения!»',
         '"It\'s not about respect. I simply see it differently."', '«Дело не в уважении. Я просто вижу это иначе.»'),
        ('homework', 'домашнее задание',
         'Parents push, children delay. Nagging makes the child angry, and delaying makes the parent angry.',
         'Родители подгоняют, дети откладывают. Нытьё злит ребёнка, а откладывание злит родителя.',
         '"Have you done your homework yet?"', '«Ты уже сделал домашнее задание?»',
         '"I\'ll do it later!" - "You said that an hour ago!"', '«Я сделаю позже!» - «Ты так говорил час назад!»'),
        ('silly annoyances', 'глупые раздражители',
         'Tiny habits repeated a thousand times, such as slurping tea or leaving lights on, slowly drive people crazy.',
         'Мелкие привычки, повторяемые тысячу раз, например хлюпать чаем или оставлять свет включённым, постепенно сводят с ума.',
         '"Do you have to slurp your tea every single time?"', '«Тебе обязательно хлюпать чаем каждый раз?»',
         '"Here we go again with the tiny things!"', '«Опять начинаются эти мелочи!»'),
        ('household chores', 'домашние дела',
         'One person feels they do more work than the other. The problem is not the dishes, it is fairness.',
         'Один человек чувствует, что делает больше другого. Проблема не в посуде, а в справедливости.',
         '"I always do the dishes! When did you last do them?"', '«Я всегда мою посуду! Когда ты в последний раз её мыл?»',
         '"Why is it always me who takes out the rubbish?"', '«Почему мусор всегда выношу я?»'),
        ('sport', 'спорт',
         'Fans of rival teams take the game too seriously, or one partner spends every weekend watching matches.',
         'Болельщики команд-соперников слишком серьёзно относятся к игре, или один из партнёров проводит все выходные у телевизора.',
         '"Your team was just lucky, that\'s all!"', '«Вашей команде просто повезло, вот и всё!»',
         '"You spent the whole weekend in front of the TV again!"', '«Ты опять провёл все выходные перед телевизором!»'),
        ('in-laws', 'родственники мужа / жены',
         'They may give unwanted advice, criticise, or visit too often, and you are stuck in the middle.',
         'Они могут давать непрошеные советы, критиковать или слишком часто приходить в гости, а вы оказываетесь между двух огней.',
         '"Your mother criticised my cooking again!"', '«Твоя мама снова раскритиковала мою стряпню!»',
         '"She means well. You take everything personally."', '«Она хочет как лучше. Ты всё принимаешь на свой счёт.»'),
        ('stress and tiredness', 'стресс и усталость',
         'Tired people are short-tempered, so a small thing suddenly becomes huge.',
         'Уставшие люди вспыльчивы, поэтому мелочь вдруг становится огромной проблемой.',
         '"I\'ve had a terrible day, so please don\'t start."', '«У меня был ужасный день, так что, пожалуйста, не начинай.»',
         '"Sorry I snapped at you. I\'m exhausted."', '«Прости, что огрызнулся. Я совершенно вымотан.»'),
        ('kids', 'дети',
         'Parents disagree about rules: screen time, sweets, bedtime, school, and who is too strict.',
         'Родители расходятся во мнениях о правилах: время у экрана, сладости, время сна, школа, и кто слишком строг.',
         '"You let them eat sweets before dinner again!"', '«Ты опять разрешил им есть сладкое перед ужином!»',
         '"A little treat won\'t hurt them!"', '«Маленькое лакомство им не повредит!»'),
        ('time spent together', 'время, проведённое вместе',
         'One person feels ignored, while the other needs personal space or is busy.',
         'Один чувствует себя игнорируемым, а другому нужно личное пространство, или он занят.',
         '"We never do anything together any more!"', '«Мы больше никогда ничего не делаем вместе!»',
         '"I\'m working hard for our future. Give me a break!"', '«Я много работаю ради нашего будущего. Дай мне передохнуть!»'),
        ('money', 'деньги',
         'Savers and spenders see the world differently, and money is linked to trust, freedom and power.',
         'Те, кто копит, и те, кто тратит, видят мир по-разному, а деньги связаны с доверием, свободой и властью.',
         '"You spent how much on a pair of shoes?!"', '«Сколько ты потратил на пару обуви?!»',
         '"It\'s my money, and I worked for it!"', '«Это мои деньги, и я их заработал!»'),
        ('work', 'работа',
         'Long hours, unfair workloads, missed deadlines, or bringing problems home can cause tension with colleagues and family.',
         'Длинный рабочий день, несправедливая нагрузка, сорванные сроки или проблемы, принесённые домой, вызывают напряжение с коллегами и семьёй.',
         '"You were supposed to send me the report yesterday!"', '«Ты должен был прислать мне отчёт вчера!»',
         '"I never got your message!"', '«Я не получал твоего сообщения!»'),
    ]
    cols = [CORAL, ORANGE, TEAL, BLUE, PURPLE]
    for i, (t, tr, e1, r1, q1, qr1, q2, qr2) in enumerate(topics):
        c = cols[i % 5]
        head = Paragraph('<font color="white"><b>%d. %s</b></font><br/><font color="white" size="7.5">%s</font>' % (i + 1, t.upper(), tr),
                         ParagraphStyle('th', fontName='DV-B', fontSize=8.6, leading=11))
        body = [*pair('<b>How it leads to arguments:</b> ' + e1, '<b>Как это приводит к спорам:</b> ' + r1, True),
                *pair('<b>Might say:</b> ' + q1, '<b>Могут сказать:</b> ' + qr1, True),
                *pair(q2, qr2, True)]
        row = Table([[head, body]], colWidths=[3.3 * cm, W - 3.3 * cm])
        row.setStyle(TableStyle([('BACKGROUND', (0, 0), (0, 0), C(c)), ('BACKGROUND', (1, 0), (1, 0), C('#f8f8fc')),
                                 ('BOX', (0, 0), (-1, -1), 0.8, C(c)),
                                 ('VALIGN', (0, 0), (0, 0), 'MIDDLE'), ('VALIGN', (1, 0), (1, 0), 'TOP'),
                                 ('LEFTPADDING', (0, 0), (-1, -1), 6), ('TOPPADDING', (0, 0), (-1, -1), 4),
                                 ('BOTTOMPADDING', (0, 0), (-1, -1), 1), ('ROUNDEDCORNERS', [7, 7, 7, 7])]))
        s.append(KeepTogether([row, sp(5)]))

    # ---------- Exercise 3 ----------
    s.append(CondPageBreak(9 * cm))
    s.append(sp(4))
    s.append(pill('EXERCISE 3 · Which three areas cause the worst arguments? Why?  /  Какие три сферы вызывают худшие ссоры? Почему?', PURPLE))
    s.append(sp(6))
    s.append(pair('<b>My answer:</b> I think the three worst areas are <b>money</b>, <b>religion</b> and <b>in-laws</b>.',
                  '<b>Мой ответ:</b> Я думаю, что три худшие сферы - это <b>деньги</b>, <b>религия</b> и <b>родственники мужа / жены</b>.')[0:2])
    s.append(sp(3))
    blocks = [
        ('1 · MONEY / ДЕНЬГИ', GREEN, GREEN_L,
         [('Money touches everything: food, housing, holidays and the future. Nobody can avoid it.', 'Деньги касаются всего: еды, жилья, отпуска и будущего. Их никто не может избежать.'),
          ('People have different habits, so the saver thinks the spender is irresponsible, and the spender thinks the saver is mean.', 'У людей разные привычки, поэтому тот, кто копит, считает расточителя безответственным, а расточитель считает копилку скупым.'),
          ('It is also about trust and power: "Who decides?" Example: "You promised we would save, and now you\'ve bought a new phone!"', 'Это ещё и вопрос доверия и власти: «Кто решает?» Пример: «Ты обещал, что мы будем копить, а теперь купил новый телефон!»')]),
        ('2 · RELIGION / РЕЛИГИЯ', PURPLE, PURPLE_L,
         [('Religion is about values, not simple opinions. Nobody wants to change their core beliefs, so nobody gives in.', 'Религия - это ценности, а не простые мнения. Никто не хочет менять основные убеждения, поэтому никто не уступает.'),
          ('You cannot "split the difference" about what you believe, and each side feels personally attacked.', 'Нельзя «найти компромисс» в том, во что вы верите, и каждая сторона чувствует личное нападение.'),
          ('Example: "How can you say that about something so important to me?"', 'Пример: «Как ты можешь так говорить о том, что так важно для меня?»')]),
        ('3 · IN-LAWS / РОДСТВЕННИКИ МУЖА (ЖЕНЫ)', BLUE, BLUE_L,
         [('You are caught in the middle between your partner and your own family. If you defend one, you hurt the other.', 'Вы оказываетесь между партнёром и собственной семьёй. Если вы защищаете одного, вы обижаете другого.'),
          ('The problem repeats at every family event, so it never completely disappears.', 'Проблема повторяется на каждом семейном событии, поэтому она никогда полностью не исчезает.'),
          ('Example: "Your mother always tells me how to raise our children!"', 'Пример: «Твоя мама всегда учит меня, как воспитывать наших детей!»')]),
    ]
    for t, c, l, its in blocks:
        s.append(KeepTogether([pill(t, c, size=9), sp(2), bullets(its, c, small=True), sp(5)]))
    s.append(box([*pair('<b>Bonus tip:</b> Stress and tiredness are not the worst topics by themselves, but they make EVERY other argument worse. If you are tired, hungry or angry, postpone the talk until tomorrow.',
                        '<b>Бонус:</b> Стресс и усталость сами по себе не худшие темы, но они ухудшают КАЖДЫЙ другой спор. Если вы устали, голодны или злы, отложите разговор до завтра.', True)],
                 YELLOW_L, ORANGE))
    return s
Writing File

~/handbook/src/part2.py

from lib import *

def build():
    s = []
    s.append(banner('PART 2 · DEVELOPING CONVERSATIONS: Defending and justifying',
                    'Two magic tools for the moment when someone says: "You did something wrong!"',
                    'Два волшебных инструмента на тот момент, когда вам говорят: «Ты поступил плохо!»', TEAL))
    s.append(sp(8))

    # story
    s.append(box([
        Paragraph('<b>The tiny courtroom / Маленький зал суда</b>', ParagraphStyle('t', parent=EN, textColor=C(TEAL))),
        *pair('Imagine you are in a tiny courtroom. Someone says: "You were late! You forgot! You spent our money!" You are your own lawyer, and you have two tools in your bag.',
              'Представьте маленький зал суда. Кто-то говорит: «Ты опоздал! Ты забыл! Ты потратил наши деньги!» Вы - собственный адвокат, и в вашей сумке два инструмента.')], TEAL_L, TEAL))
    s.append(sp(8))

    t1 = box([Paragraph('<b>TOOL 1: THE SHIELD</b>', ParagraphStyle('a', parent=EN, textColor=C(BLUE), fontSize=10.5)),
              Paragraph('<b>It\'s not as though / as if ...</b>', ParagraphStyle('a2', parent=EN, fontSize=10.5)),
              *pair('Meaning: "Do not think that I did something terrible. That idea is NOT true."',
                    'Смысл: «Не думай, что я сделал что-то ужасное. Это НЕ так.»', True),
              *pair('Example: It\'s not as though I did it deliberately. (= I did not do it deliberately.)',
                    'Пример: Не то чтобы я сделал это нарочно. (= Я сделал это не нарочно.)', True)],
             BLUE_L, BLUE, width=W / 2 - 4)
    t2 = box([Paragraph('<b>TOOL 2: THE EXPLANATION CARD</b>', ParagraphStyle('b', parent=EN, textColor=C(ORANGE), fontSize=10.5)),
              Paragraph('<b>It\'s just that ...</b>', ParagraphStyle('b2', parent=EN, fontSize=10.5)),
              *pair('Meaning: "Here is the real reason. It is simply this."',
                    'Смысл: «Вот настоящая причина. Всё просто вот в чём.»', True),
              *pair('Example: It\'s just that it\'s been a long day and this was the last straw.',
                    'Пример: Просто день был долгим, и это стало последней каплей.', True)],
             ORANGE_L, ORANGE, width=W / 2 - 4)
    tt = Table([[t1, t2]], colWidths=[W / 2, W / 2])
    tt.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ('LEFTPADDING', (0, 0), (-1, -1), 0),
                            ('RIGHTPADDING', (0, 0), (-1, -1), 0)]))
    s.append(tt)
    s.append(sp(9))

    # recipe
    s.append(pill('THE 3-STEP RECIPE  /  Рецепт из трёх шагов', TEAL))
    s.append(sp(4))
    s.append(bullets([
        ('<b>Step 1 - Soften:</b> say "I know", "OK" or "Sorry". This shows you are not fighting.',
         '<b>Шаг 1 - Смягчите:</b> скажите «Я знаю», «Хорошо» или «Прости». Это показывает, что вы не воюете.'),
        ('<b>Step 2 - Shield:</b> It\'s not as though / as if + a complete sentence (the bad idea you reject).',
         '<b>Шаг 2 - Щит:</b> It\'s not as though / as if + полное предложение (плохая мысль, которую вы отвергаете).'),
        ('<b>Step 3 - Card:</b> It\'s just that + a complete sentence (the real reason).',
         '<b>Шаг 3 - Карточка:</b> It\'s just that + полное предложение (настоящая причина).'),
    ], TEAL))
    s.append(sp(4))
    s.append(dialogue([
        ('A', '"You\'re an hour late!"', '«Ты опоздал на час!»'),
        ('B', '"I know, I\'m sorry. <b>It\'s not as though</b> I wanted to be late. <b>It\'s just that</b> the bus broke down."',
         '«Я знаю, прости. Не то чтобы я хотел опоздать. Просто автобус сломался.»'),
    ], BLUE, ORANGE, BLUE_L, ORANGE_L))
    s.append(sp(9))

    # grammar notes
    s.append(pill('EASY GRAMMAR NOTES  /  Простые грамматические заметки', ORANGE))
    s.append(sp(4))
    s.append(bullets([
        ('<b>as though = as if.</b> They mean the same. In informal speech people also say "It\'s not like ..." (very common in American English).',
         '<b>as though = as if.</b> Значение одинаково. В разговорной речи также говорят «It\'s not like ...» (очень часто в американском английском).'),
        ('After the Shield you need a <b>full sentence</b> (subject + verb): It\'s not as though <u>I</u> <u>did</u> it on purpose.',
         'После «щита» нужно <b>полное предложение</b> (подлежащее + глагол): It\'s not as though I did it on purpose.'),
        ('After the Card you also need a <b>full sentence</b>: It\'s just that <u>I</u> <u>was</u> tired. Do not add "because": it already means "the reason is".',
         'После «карточки» тоже нужно <b>полное предложение</b>: It\'s just that I was tired. Не добавляйте «because»: оно уже означает «причина в том, что».'),
        ('The Shield often contains a negative idea or an idea you reject: "It\'s not as though I don\'t care" = "I DO care."',
         'Щит часто содержит отрицательную мысль или мысль, которую вы отвергаете: «It\'s not as though I don\'t care» = «Мне НЕ всё равно».'),
        ('Tone matters. With a soft voice it sounds honest. With a loud, angry voice it sounds like an excuse.',
         'Тон важен. Мягким голосом это звучит честно. Громким, злым голосом это звучит как отговорка.'),
    ], ORANGE, small=True))
    s.append(sp(9))

    # Examples
    s.append(CondPageBreak(10 * cm))
    s.append(pill('SHIELD SENTENCES  /  Предложения-«щиты»  It\'s not as though / if ...', BLUE))
    s.append(sp(4))
    s.append(bullets([
        ('It\'s not as though I wanted to miss your birthday.', 'Не то чтобы я хотел пропустить твой день рождения.'),
        ('It\'s not as if I do this every day.', 'Не то чтобы я делаю это каждый день.'),
        ('It\'s not as though I\'m lazy. I\'ve worked all week.', 'Не то чтобы я ленивый. Я работал всю неделю.'),
        ('It\'s not as though anyone was using the room.', 'Не то чтобы кто-то пользовался этой комнатой.'),
        ('It\'s not as though we\'re rich, you know.', 'Мы же не богачи, знаешь ли.'),
        ('It\'s not as if you were doing anything important.', 'Ты же не делал ничего важного.'),
        ('It\'s not as though I had a choice.', 'Не то чтобы у меня был выбор.'),
        ('It\'s not like I asked you to carry it for me. (informal)', 'Я же не просил тебя нести это за меня. (разговорный стиль)'),
        ('It\'s not as though I don\'t like your cooking.', 'Не то чтобы мне не нравилась твоя стряпня.'),
        ('It\'s not as if the mistake cost us a lot of money.', 'Не то чтобы эта ошибка стоила нам больших денег.'),
    ], BLUE, small=True))
    s.append(sp(8))
    s.append(pill('CARD SENTENCES  /  Предложения-«карточки»  It\'s just that ...', ORANGE))
    s.append(sp(4))
    s.append(bullets([
        ('It\'s just that I\'ve had a really long day.', 'Просто у меня был очень длинный день.'),
        ('It\'s just that I\'m worried about money.', 'Просто я беспокоюсь о деньгах.'),
        ('It\'s just that my phone died and I couldn\'t call you.', 'Просто мой телефон разрядился, и я не смог тебе позвонить.'),
        ('It\'s just that I thought you would be angry with me.', 'Просто я думал, что ты рассердишься на меня.'),
        ('It\'s just that this is the third time this week.', 'Просто это уже третий раз на этой неделе.'),
        ('It\'s just that I miss you when you\'re away.', 'Просто я скучаю по тебе, когда тебя нет.'),
        ('It\'s just that I don\'t feel very well today.', 'Просто сегодня я неважно себя чувствую.'),
        ('It\'s just that I didn\'t want to disturb you.', 'Просто я не хотел тебя беспокоить.'),
        ('It\'s just that the deadline is tomorrow and I\'m stressed.', 'Просто крайний срок завтра, и я на нервах.'),
        ('It\'s just that nobody told me about the change.', 'Просто никто не сказал мне об изменении.'),
    ], ORANGE, small=True))
    s.append(sp(9))

    # mini dialogues
    s.append(CondPageBreak(8 * cm))
    s.append(pill('SHIELD + CARD IN REAL LIFE: 8 MINI-SCENES  /  8 мини-сцен из жизни', TEAL))
    s.append(sp(5))
    scenes = [
        ('1 · Chores / Домашние дела', [
            ('A', 'You didn\'t wash the dishes again!', 'Ты опять не помыл посуду!'),
            ('B', 'Come on, it\'s not as if I never do them. It\'s just that I had a deadline today.', 'Ну брось, не то чтобы я никогда их не мыл. Просто у меня сегодня был срок сдачи.')]),
        ('2 · Money / Деньги', [
            ('A', 'Why did you buy another jacket?', 'Зачем ты купил ещё одну куртку?'),
            ('B', 'What\'s the problem? It\'s not as though I buy clothes every week. It\'s just that this one was on sale.', 'В чём проблема? Не то чтобы я покупаю одежду каждую неделю. Просто эта была со скидкой.')]),
        ('3 · Phone / Телефон', [
            ('A', 'You\'re always on your phone!', 'Ты всё время в телефоне!'),
            ('B', 'It\'s not as though I\'m ignoring you. It\'s just that my boss keeps texting me.', 'Не то чтобы я тебя игнорирую. Просто мой начальник постоянно мне пишет.')]),
        ('4 · Dinner / Ужин', [
            ('A', 'You didn\'t like the dinner, did you?', 'Тебе не понравился ужин, да?'),
            ('B', 'It\'s not as though it was bad. It\'s just that I\'m not very hungry tonight.', 'Не то чтобы он был плохим. Просто я сегодня не очень голоден.')]),
        ('5 · Study / Учёба', [
            ('A', 'Why didn\'t you study for the test?', 'Почему ты не готовился к тесту?'),
            ('B', 'It\'s not as if I didn\'t want to. It\'s just that I couldn\'t concentrate.', 'Не то чтобы я не хотел. Просто я не мог сосредоточиться.')]),
        ('6 · Friendship / Дружба', [
            ('A', 'You never call me any more!', 'Ты мне больше никогда не звонишь!'),
            ('B', 'That\'s not fair. It\'s not as though I don\'t care. It\'s just that I\'m swamped at work.', 'Это несправедливо. Не то чтобы мне всё равно. Просто я завален работой.')]),
        ('7 · Shouting / Крик', [
            ('A', 'There\'s no need to shout!', 'Не нужно кричать!'),
            ('B', 'You\'re right. It\'s not as though I\'m angry at you. It\'s just that it\'s been a terrible day.', 'Ты права. Не то чтобы я злюсь на тебя. Просто это был ужасный день.')]),
        ('8 · Broken cup / Разбитая чашка', [
            ('A', 'You broke my favourite cup!', 'Ты разбил мою любимую чашку!'),
            ('B', 'I\'m so sorry. It\'s not as though I did it deliberately. It\'s just that it slipped out of my hand.', 'Мне очень жаль. Не то чтобы я сделал это нарочно. Просто она выскользнула у меня из рук.')]),
    ]
    for ttl, rows in scenes:
        s.append(KeepTogether([Paragraph('<b>%s</b>' % ttl, ParagraphStyle('sc', parent=EN_S, textColor=C(TEAL))),
                               sp(2), dialogue(rows, BLUE, ORANGE, BLUE_L, ORANGE_L), sp(6)]))

    # mistakes
    s.append(CondPageBreak(6 * cm))
    s.append(pill('COMMON MISTAKES  /  Частые ошибки', CORAL))
    s.append(sp(4))
    s.append(grid([
        ['Wrong ✗ / Неверно', 'Correct ✓ / Верно', 'Why / Почему'],
        ['It\'s just that because I was tired.', 'It\'s just that I was tired.',
         'No "because". / Не нужно «because».'],
        ['It\'s not as though I no like you.', 'It\'s not as though I don\'t like you.',
         'Use a normal negative. / Используйте обычное отрицание.'],
        ['It\'s not as though doing it on purpose.', 'It\'s not as though I did it on purpose.',
         'Full sentence with a subject. / Полное предложение с подлежащим.'],
        ['It\'s just that, I was tired.', 'It\'s just that I was tired.',
         'No comma after "that". / Без запятой после «that».'],
    ], [W * 0.36, W * 0.36, W * 0.28], CORAL))
    s.append(sp(9))

    # Ex 8 answers
    s.append(CondPageBreak(9 * cm))
    s.append(pill('PRACTICE · Exercise 8 (page 57): sample answers  /  Образцы ответов к упражнению 8', PURPLE))
    s.append(sp(5))
    ex8 = [
        ('1', [('A', 'There\'s no need to shout!', 'Не нужно кричать!'),
               ('B', 'I know. I\'m sorry. It\'s just that I\'ve been waiting for an answer for two hours.', 'Я знаю. Прости. Просто я жду ответа уже два часа.')]),
        ('2', [('A', 'Why did you buy that?', 'Зачем ты это купил?'),
               ('B', 'What\'s the problem? It\'s not as if it was expensive.', 'В чём проблема? Не то чтобы это было дорого.'),
               ('A', 'I know. It\'s just that we agreed to save money this month.', 'Я знаю. Просто мы договорились экономить в этом месяце.')]),
        ('3', [('A', 'Why can\'t someone else do it? It\'s not as though I\'m the only one who works here.', 'Почему не может сделать кто-то другой? Не то чтобы я один здесь работал.'),
               ('B', 'I know. It\'s just that you\'re the only one who knows how the system works.', 'Я знаю. Просто ты единственный, кто знает, как работает эта система.')]),
        ('4', [('A', 'I should\'ve asked you, but I was in a hurry.', 'Мне следовало спросить тебя, но я торопился.'),
               ('B', 'OK, but it\'s not as though I would have said no.', 'Ладно, но не то чтобы я сказал бы «нет».'),
               ('A', 'I guess. It\'s just that you were in a meeting and I didn\'t want to interrupt.', 'Наверное. Просто ты был на совещании, и я не хотел мешать.')]),
        ('5', [('A', 'I can\'t believe you did that.', 'Не могу поверить, что ты это сделал.'),
               ('B', 'It\'s not as though I had a choice. It\'s just that nobody else was there to help.', 'Не то чтобы у меня был выбор. Просто там больше никого не было, чтобы помочь.')]),
    ]
    for n, rows in ex8:
        s.append(KeepTogether([Paragraph('<b>Conversation %s</b>' % n, ParagraphStyle('sc', parent=EN_S, textColor=C(PURPLE))),
                               sp(2), dialogue(rows, BLUE, ORANGE, BLUE_L, ORANGE_L), sp(6)]))

    # quiz
    s.append(CondPageBreak(9 * cm))
    s.append(pill('MINI-QUIZ  /  Мини-тест: choose "It\'s not as though" or "It\'s just that"', GREEN))
    s.append(sp(4))
    s.append(bullets([
        ('1. I\'m sorry I snapped. ______ I\'m really stressed.', '1. Прости, что я огрызнулся. Просто я очень напряжён.'),
        ('2. ______ I did it on purpose!', '2. Не то чтобы я сделал это нарочно!'),
        ('3. I forgot your message. ______ my phone was off all day.', '3. Я не увидел твоё сообщение. Просто мой телефон весь день был выключен.'),
        ('4. ______ I don\'t trust you.', '4. Не то чтобы я тебе не доверял.'),
        ('5. ______ it was a big mistake, but I\'m still embarrassed.', '5. Не то чтобы это была большая ошибка, но мне всё равно неловко.'),
        ('6. I didn\'t answer. ______ I was in the middle of a meeting.', '6. Я не ответил. Просто я был посреди совещания.'),
    ], GREEN, small=True))
    s.append(sp(3))
    s.append(box([*pair('<b>Answers / Ответы:</b> 1 It\'s just that · 2 It\'s not as though · 3 It\'s just that · 4 It\'s not as though · 5 It\'s not as though · 6 It\'s just that.',
                        '<b>Ответы:</b> 1 Просто · 2 Не то чтобы · 3 Просто · 4 Не то чтобы · 5 Не то чтобы · 6 Просто.', True)], GREEN_L, GREEN))
    return s
Writing File

~/handbook/src/part3.py

from lib import *

def build():
    s = []
    s.append(banner('PART 3 · GRAMMAR: Ellipsis and substitution',
                    'Do not say it twice! English loves short answers and hidden words.',
                    'Не говорите дважды! В английском любят короткие ответы и «спрятанные» слова.', PURPLE))
    s.append(sp(8))
    s.append(box([
        Paragraph('<b>The big idea / Главная идея</b>', ParagraphStyle('t', parent=EN, textColor=C(PURPLE))),
        *pair('Imagine you are sending a text message and every word costs money. You would delete everything that the other person already knows. English does the same thing.',
              'Представьте, что вы отправляете сообщение, и каждое слово стоит денег. Вы бы убрали всё, что собеседник уже знает. В английском происходит то же самое.'),
        *pair('<b>Ellipsis</b> = we leave words out (they become invisible "ghost words", but everybody understands them).',
              '<b>Эллипсис</b> = мы пропускаем слова (они становятся невидимыми «словами-призраками», но все их понимают).', True),
        *pair('<b>Substitution</b> = we replace a long phrase with a tiny stand-in word (do, did, so, one, that).',
              '<b>Замена (подстановка)</b> = мы заменяем длинную фразу маленьким словом-заменителем (do, did, so, one, that).', True)],
        PURPLE_L, PURPLE))
    s.append(sp(8))
    s.append(pill('A · THE AUXILIARY IS A "REMOTE-CONTROL BUTTON"  /  Вспомогательный глагол - «кнопка пульта»', PURPLE))
    s.append(sp(4))
    s.extend(pair('Instead of saying the whole verb phrase again, we press one button: an auxiliary verb (be, do, have, will, would, can, should ...). The auxiliary stands for the whole phrase.',
                  'Вместо того чтобы повторять всю глагольную группу, мы нажимаем одну кнопку: вспомогательный глагол (be, do, have, will, would, can, should ...). Он заменяет всю группу.'))
    s.append(grid([
        ['Full sentence / Полное предложение', 'Short form / Краткая форма', 'Hidden words / Скрытые слова'],
        ['Do you like tea? Yes, I like tea.', 'Yes, I <b>do</b>.', 'like tea'],
        ['Are you coming? Yes, I am coming.', 'Yes, I <b>am</b>.', 'coming'],
        ['Has she finished? No, she hasn\'t finished.', 'No, she <b>hasn\'t</b>.', 'finished'],
        ['Can you swim? Yes, I can swim.', 'Yes, I <b>can</b>.', 'swim'],
        ['Who ate my cake? I ate it.', 'I <b>did</b>.', 'ate your cake'],
        ['Call me later. I will call you later.', 'I <b>will</b>.', 'call you later'],
    ], [W * 0.42, W * 0.26, W * 0.32], PURPLE))
    s.append(sp(4))
    s.extend(pair('Do you like tea? Yes, I do.  /  Are you coming? Yes, I am.  /  Has she finished? No, she hasn\'t.',
                  'Ты любишь чай? Да.  /  Ты придёшь? Да.  /  Она закончила? Нет.', True))
    s.append(sp(6))

    s.append(box([*pair('<b>Golden rule:</b> the auxiliary shows the TIME and the FEELING of the new sentence. So it may be different from the original verb!',
                        '<b>Золотое правило:</b> вспомогательный глагол показывает ВРЕМЯ и ОТНОШЕНИЕ нового предложения. Поэтому он может отличаться от исходного глагола!', True),
                  *pair('A: I was going to take it to my room. B: I wish you <b>had</b>. (had = had taken it. Regret about the past, so we use past perfect.)',
                        'А: Я собирался отнести это в свою комнату. Б: Жаль, что ты не отнёс. (had = had taken. Сожаление о прошлом, поэтому past perfect.)', True)],
                 YELLOW_L, ORANGE))
    s.append(sp(9))

    # Ex 10
    s.append(CondPageBreak(10 * cm))
    s.append(pill('EXERCISE 10 (page 57) · The Grammar box, solved  /  Упражнение 10: решение', BLUE))
    s.append(sp(5))
    s.append(grid([
        ['Sentence / Предложение', 'Auxiliary', 'What it refers to (full phrase) / На что указывает'],
        ['B: Well, I wish you <b>had</b>.', 'had', 'had taken it to your room (wish about the past). Я бы хотел, чтобы ты отнёс это в свою комнату.'],
        ['D: I <b>would have</b>, but you hardly come out of that office.', 'would have', 'would have said something sooner. Я бы сказал раньше, но ты почти не выходишь из офиса.'],
        ['E: If I take my lunch break when you <b>do</b> ...', 'do', 'take your lunch break. Если я беру обеденный перерыв, когда ты берёшь...'],
        ['F: OK, let\'s <b>do</b> that.', 'do (that)', 'take our breaks at the same time and go shopping. Давай так и сделаем.'],
    ], [W * 0.38, W * 0.14, W * 0.48], BLUE))
    s.append(sp(5))
    s.append(bullets([
        ('<b>Q1. What do the auxiliaries refer to?</b> They refer back to the verb phrases in the table above (taken it to your room / said something sooner / take your lunch break / take breaks together).',
         '<b>В1. На что указывают вспомогательные?</b> На глагольные группы из таблицы выше (отнёс это в комнату / сказал раньше / взять обеденный перерыв / брать перерыв вместе).'),
        ('<b>Q2. The full verb phrase would be:</b> "I wish you had taken it to your room", "I would have said something sooner", "when you take your lunch break".',
         '<b>В2. Полная глагольная группа была бы:</b> «I wish you had taken it to your room», «I would have said something sooner», «when you take your lunch break».'),
        ('<b>Q3. Same or different form?</b> Sometimes different: take (original) becomes <b>had</b> (taken), and the phrase "said something" becomes <b>would have</b>. Sometimes the same: take becomes <b>do</b> (present). The auxiliary follows the time and meaning of the NEW sentence, not the old one.',
         '<b>В3. Форма та же или другая?</b> Иногда другая: take (исходное) превращается в <b>had</b> (taken), а «said something» в <b>would have</b>. Иногда та же: take превращается в <b>do</b> (настоящее). Вспомогательный глагол следует времени и смыслу НОВОГО предложения, а не старого.'),
    ], BLUE, small=True))
    s.append(sp(9))

    # Ex 11
    s.append(pill('EXERCISE 11 (page 57) · Complete the sentences, with functions  /  Упражнение 11: ответы и функции', GREEN))
    s.append(sp(5))
    ex11 = [
        ('1', [('A', 'What? You\'re joking?', 'Что? Ты шутишь?'),
               ('B', 'I wish I <b>were</b> (or was).', 'Хотел бы я, чтобы это была шутка.')],
         'Function: expressing a wish about a sad reality (I am NOT joking, but I wish I were).',
         'Функция: выражение желания о печальной реальности (я НЕ шучу, но хотел бы, чтобы это было шуткой).'),
        ('2', [('A', 'You should\'ve tried calling them.', 'Тебе следовало попробовать им позвонить.'),
               ('B', 'I <b>did</b>, but no-one answered.', 'Я пробовал, но никто не ответил.')],
         'Function: defending yourself against criticism (I already did it!).',
         'Функция: защита от критики (я это уже сделал!).'),
        ('3', [('A', 'Call me later.', 'Позвони мне позже.'),
               ('B', 'I <b>will</b>.', 'Позвоню.')],
         'Function: a promise or agreement.', 'Функция: обещание или согласие.'),
        ('4', [('A', 'You should\'ve booked the ticket when I <b>did</b>. You said you were going to.', 'Тебе следовало забронировать билет, когда это сделал я. Ты говорил, что собираешься.'),
               ('B', 'I <b>was</b>, but then I got distracted and it slipped my mind. Sorry.', 'Собирался, но потом отвлёкся, и это вылетело у меня из головы. Прости.')],
         'Function: "did" avoids repeating "booked the ticket"; "was" (going to) refers back to an earlier plan and gives an excuse.',
         'Функция: «did» избегает повторения «booked the ticket»; «was» (going to) отсылает к прежнему плану и служит оправданием.'),
    ]
    for n, rows, fe, fr in ex11:
        s.append(KeepTogether([Paragraph('<b>Sentence %s</b>' % n, ParagraphStyle('sc', parent=EN_S, textColor=C(GREEN))), sp(2),
                               dialogue(rows, GREEN, BLUE, GREEN_L, BLUE_L), sp(2), *pair(fe, fr, True), sp(5)]))

    # Section C
    s.append(PageBreak())
    s.append(pill('B · MORE TOOLS IN THE ELLIPSIS & SUBSTITUTION TOOLBOX  /  Другие инструменты', ORANGE))
    s.append(sp(5))
    tools = [
        ('1 · Leave out the repeated subject after and / but / or  /  Пропуск повторяющегося подлежащего', ORANGE, ORANGE_L, [
            ('She opened the door and [she] walked in.', 'Она открыла дверь и вошла.'),
            ('I wanted to call, but [I] forgot.', 'Я хотел позвонить, но забыл.'),
            ('He can sing and [he can] dance.', 'Он умеет петь и танцевать.')]),
        ('2 · Leave out the verb after "to"  /  Пропуск глагола после «to»', BLUE, BLUE_L, [
            ('Do you want to come? I\'d love to [come].', 'Хочешь пойти? Я бы с удовольствием.'),
            ('I didn\'t go because I didn\'t want to [go].', 'Я не пошёл, потому что не хотел.'),
            ('You can stay if you like [to stay].', 'Можешь остаться, если хочешь.')]),
        ('3 · so / not replace a whole clause  /  so / not заменяют целое предложение', GREEN, GREEN_L, [
            ('Will it rain? I think <b>so</b>. (= I think it will rain.)', 'Будет дождь? Думаю, да.'),
            ('Is he angry? I hope <b>not</b>. (= I hope he isn\'t angry.)', 'Он сердится? Надеюсь, нет.'),
            ('Is the exam hard? I\'m afraid <b>so</b>.', 'Экзамен сложный? Боюсь, что да.'),
            ('Are you free tonight? If <b>so</b>, let\'s meet. If <b>not</b>, tomorrow.', 'Ты свободен сегодня вечером? Если да, давай встретимся. Если нет, то завтра.')]),
        ('4 · one / ones replace a noun  /  one / ones заменяют существительное', PURPLE, PURPLE_L, [
            ('Which bag is yours? The red <b>one</b>.', 'Какая сумка твоя? Красная.'),
            ('My shoes are old. I need new <b>ones</b>.', 'Мои туфли старые. Мне нужны новые.'),
            ('I\'ll have the same. (= the same thing)', 'Мне то же самое.')]),
        ('5 · do it / do so / do that  /  заменяют действие', CORAL, CORAL_L, [
            ('Can you fix the tap? I\'ll <b>do it</b> tomorrow.', 'Ты можешь починить кран? Я сделаю это завтра.'),
            ('He told me to apologise, but I refuse to <b>do so</b>. (formal)', 'Он сказал мне извиниться, но я отказываюсь это делать. (формально)'),
            ('Let\'s go shopping. OK, let\'s <b>do that</b>.', 'Пойдём по магазинам. Хорошо, так и сделаем.')]),
        ('6 · So do I / Neither do I  /  Я тоже / Я тоже нет', TEAL, TEAL_L, [
            ('I love pizza. - <b>So do I</b>.', 'Я люблю пиццу. - Я тоже.'),
            ('I can\'t swim. - <b>Neither can I</b>.', 'Я не умею плавать. - Я тоже не умею.'),
            ('I\'ve been to Paris. - <b>So have I</b>.', 'Я был в Париже. - Я тоже.'),
            ('She wasn\'t at the party. - <b>Neither was he</b>.', 'Её не было на вечеринке. - Его тоже.')]),
        ('7 · Comparisons: than I do / than I am  /  Сравнения', BLUE, BLUE_L, [
            ('She runs faster than I <b>do</b>.', 'Она бегает быстрее, чем я.'),
            ('He is taller than I <b>am</b>.', 'Он выше, чем я.'),
            ('They earn more than we <b>did</b> last year.', 'Они зарабатывают больше, чем мы в прошлом году.')]),
        ('8 · Everyday spoken ellipsis  /  Разговорные сокращения', ORANGE, ORANGE_L, [
            ('[Have you] Seen my keys?', 'Ты не видел мои ключи?'),
            ('[That] Sounds great!', 'Звучит здорово!'),
            ('[Would you like some] Coffee?', 'Хочешь кофе?'),
            ('[Is] Anybody home?', 'Дома кто-нибудь есть?'),
            ('[I] Can\'t believe it!', 'Не могу поверить!')]),
    ]
    for ttl, c, l, its in tools:
        s.append(KeepTogether([pill(ttl, c, size=8.8), sp(3), bullets(its, c, small=True), sp(6)]))

    # mistakes
    s.append(CondPageBreak(7 * cm))
    s.append(pill('COMMON MISTAKES  /  Частые ошибки', CORAL))
    s.append(sp(4))
    s.append(grid([
        ['Wrong ✗ / Неверно', 'Correct ✓ / Верно', 'Why / Почему'],
        ['Are you ready? Yes, I\'m.', 'Yes, I <b>am</b>.', 'No contraction at the end. / Нет сокращения в конце.'],
        ['I haven\'t been there, and she doesn\'t.', '... and <b>neither has she</b>.', 'The auxiliary must match the tense. / Вспомогательный глагол должен соответствовать времени.'],
        ['She can swim and I do too.', '... and <b>so can I</b>.', 'Use the same auxiliary. / Тот же вспомогательный глагол.'],
        ['Do you want to come? Yes, I want.', 'Yes, I <b>do</b>. / I\'d love <b>to</b>.', 'Use do or to. / Используйте do или to.'],
        ['I\'d like a coffee and she\'d like a coffee.', 'I\'d like a coffee and <b>she would, too</b>.', 'Avoid repetition. / Избегайте повторов.'],
    ], [W * 0.36, W * 0.30, W * 0.34], CORAL))
    s.append(sp(9))

    # Ex 12
    s.append(CondPageBreak(10 * cm))
    s.append(pill('EXERCISE 12 (page 57) · Three-line dialogues  /  Диалоги из трёх реплик', PURPLE))
    s.append(sp(5))
    dl = [
        ('Comment 2 · You shouldn\'t have left it so late.', 'Ты не должен был оставлять это на такой поздний срок.', [
            ('A', 'You shouldn\'t have left it so late.', 'Ты не должен был оставлять это так поздно.'),
            ('B', 'I know I shouldn\'t have, but I was snowed under with work.', 'Я знаю, что не должен был, но я был завален работой.'),
            ('A', 'We all were, but the rest of us still finished on time.', 'Мы все были, но остальные всё равно закончили вовремя.')]),
        ('Comment 3 · I can\'t believe you\'re studying instead of coming to my party.', 'Не могу поверить, что ты учишься вместо того, чтобы прийти на мою вечеринку.', [
            ('A', 'I can\'t believe you\'re studying instead of coming to my party!', 'Не могу поверить, что ты учишься вместо того, чтобы прийти на мою вечеринку!'),
            ('B', 'I\'d love to, but I have an exam tomorrow.', 'Я бы с радостью, но у меня завтра экзамен.'),
            ('A', 'You said you would!', 'Ты же говорил, что придёшь!')]),
        ('Comment 4 · You were shouting and screaming.', 'Ты кричал и вопил.', [
            ('A', 'You were shouting and screaming!', 'Ты кричал и вопил!'),
            ('B', 'So were you!', 'Ты тоже!'),
            ('A', 'Only because you started it. I wouldn\'t have if you hadn\'t.', 'Только потому, что ты начал. Я бы не стал, если бы ты не начал.')]),
        ('Comment 5 · Don\'t talk to people about company business.', 'Не обсуждай с людьми дела компании.', [
            ('A', 'Don\'t talk to people about company business.', 'Не обсуждай с людьми дела компании.'),
            ('B', 'I haven\'t! Who said I had?', 'Я не обсуждал! Кто сказал, что обсуждал?'),
            ('A', 'Mark did. He said he saw you.', 'Марк сказал. Он сказал, что видел тебя.')]),
    ]
    for ttl, tr, rows in dl:
        s.append(KeepTogether([Paragraph('<b>%s</b>' % ttl, ParagraphStyle('sc', parent=EN_S, textColor=C(PURPLE))),
                               sp(2), dialogue(rows, PURPLE, ORANGE, PURPLE_L, ORANGE_L), sp(6)]))
    s.append(box([*pair('<b>Ghost words check:</b> "I shouldn\'t have [left it so late]", "We all were [snowed under]", "I\'d love to [come]", "So were you [shouting and screaming]", "Mark did [say it]".',
                        '<b>Проверка «слов-призраков»:</b> «I shouldn\'t have [left it so late]», «We all were [snowed under]», «I\'d love to [come]», «So were you [shouting and screaming]», «Mark did [say it]».', True)],
                 YELLOW_L, ORANGE))
    s.append(sp(9))

    # quiz
    s.append(CondPageBreak(9 * cm))
    s.append(pill('MINI-QUIZ  /  Мини-тест', GREEN))
    s.append(sp(4))
    s.append(bullets([
        ('1. Have you finished? Yes, I ______.', '1. Ты закончил? Да.'),
        ('2. I can\'t cook. - ______ can I.', '2. Я не умею готовить. - Я тоже не умею.'),
        ('3. Did you call her? I ______, but she didn\'t answer.', '3. Ты ей звонил? Звонил, но она не ответила.'),
        ('4. You should have told me. - I wish I ______.', '4. Тебе следовало мне сказать. - Жаль, что я не сказал.'),
        ('5. Are you going to apologise? I don\'t want ______.', '5. Ты собираешься извиниться? Не хочу.'),
        ('6. Will they come? I hope ______.', '6. Они придут? Надеюсь, нет.'),
        ('7. Which phone is yours? The black ______.', '7. Какой телефон твой? Чёрный.'),
        ('8. I\'ll pay today. - OK, you ______, and I will pay tomorrow.', '8. Сегодня плачу я. - Хорошо, ты заплатишь сегодня, а я завтра.'),
    ], GREEN, small=True))
    s.append(sp(3))
    s.append(box([*pair('<b>Answers / Ответы:</b> 1 have · 2 Neither · 3 did · 4 had · 5 to · 6 not · 7 one · 8 do / will (you will).',
                        '<b>Ответы:</b> 1 have · 2 Neither · 3 did · 4 had · 5 to · 6 not · 7 one · 8 will.', True)], GREEN_L, GREEN))
    s.append(sp(10))

    # cheat sheet
    s.append(CondPageBreak(10 * cm))
    s.append(pill('POCKET CHEAT SHEET  /  Шпаргалка', NAVY))
    s.append(sp(5))
    s.append(grid([
        ['Tool / Инструмент', 'Example / Пример', 'Use / Когда использовать'],
        ['It\'s not as though / as if ...', 'It\'s not as though I did it deliberately.', 'Reject a wrong idea. / Отвергнуть неверную мысль.'],
        ['It\'s just that ...', 'It\'s just that it\'s been a long day.', 'Give the real reason. / Назвать настоящую причину.'],
        ['Auxiliary only', 'I will. / I did. / I wish I had.', 'Avoid repeating a verb phrase. / Не повторять глагольную группу.'],
        ['so / not', 'I think so. / I hope not.', 'Replace a clause. / Заменить предложение.'],
        ['one / ones', 'The red one.', 'Replace a noun. / Заменить существительное.'],
        ['So do I / Neither do I', 'So do I. / Neither can I.', 'Agree with someone. / Согласиться.'],
        ['do it / do that / do so', 'Let\'s do that.', 'Replace an action. / Заменить действие.'],
    ], [W * 0.28, W * 0.38, W * 0.34], NAVY))
    return s
Writing File

~/handbook/src/build.py

import sys
sys.path.insert(0, '/tmp/workspace/handbook/src')
from lib import *
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
from PIL import Image as PI
import part1, part2, part3

OUT = '/tmp/workspace/handbook/output/Conflict_and_Resolution_Handbook.pdf'

# prepare photo
im = PI.open('/tmp/workspace/handbook/assets/p2_0_Im0.jpg').convert('RGB')
im.thumbnail((900, 900))
im.save('/tmp/workspace/handbook/assets/cover.jpg', quality=85)
iw, ih = im.size

def cover_page(c, doc):
    c.saveState()
    c.setFillColor(C(NAVY)); c.rect(0, 0, PW, PH, fill=1, stroke=0)
    # colourful circles
    for (x, y, r, col, a) in [(2, PH - 1, 5.5, CORAL, .9), (PW / cm - 1, PH / cm - 6, 3.5, ORANGE, .9),
                              (3, 4, 4, TEAL, .85), (PW / cm - 3, 2, 5, PURPLE, .85), (PW / cm - 1, 12, 1.6, BLUE, .9)]:
        c.setFillColor(C(col)); c.setFillAlpha(a)
        c.circle(x * cm if x > 0 else 0, y * cm if y < 100 else y, r * cm, fill=1, stroke=0)
    c.setFillAlpha(1)
    c.restoreState()

def main_page(color, label):
    def f(c, doc):
        c.saveState()
        c.setFillColor(C(color)); c.rect(0, PH - 0.55 * cm, PW, 0.55 * cm, fill=1, stroke=0)
        c.setFillColor(colors.white); c.setFont('DV-B', 7.5)
        c.drawString(MARGIN, PH - 0.38 * cm, label)
        c.setFillColor(C(color)); c.rect(0, 0, PW, 0.25 * cm, fill=1, stroke=0)
        c.setFillColor(C(GREY)); c.setFont('DV', 8)
        c.drawCentredString(PW / 2, 0.65 * cm, 'Unit 6A  Clear the air  |  Page %d' % doc.page)
        c.restoreState()
    return f

fr = Frame(MARGIN, 1.2 * cm, W, PH - 2.2 * cm, id='f', leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0)
frc = Frame(MARGIN, 1.5 * cm, W, PH - 3 * cm, id='fc', leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0)
doc = BaseDocTemplate(OUT, pagesize=A4, title='Conflict and Resolution - Colourful Handbook',
                      author='Orris', leftMargin=MARGIN, rightMargin=MARGIN)
doc.addPageTemplates([
    PageTemplate(id='cover', frames=[frc], onPage=cover_page),
    PageTemplate(id='toc', frames=[fr], onPage=main_page(NAVY, 'HANDBOOK  |  Conflict and resolution  |  Contents')),
    PageTemplate(id='p1', frames=[fr], onPage=main_page(CORAL, 'PART 1  |  SPEAKING  |  Часть 1: Говорение')),
    PageTemplate(id='p2', frames=[fr], onPage=main_page(TEAL, 'PART 2  |  DEFENDING AND JUSTIFYING  |  Часть 2: Защита и оправдание')),
    PageTemplate(id='p3', frames=[fr], onPage=main_page(PURPLE, 'PART 3  |  ELLIPSIS AND SUBSTITUTION  |  Часть 3: Эллипсис и замена')),
])

story = []
# ---- cover ----
white_big = ParagraphStyle('wb', fontName='DV-B', fontSize=31, leading=37, textColor=colors.white)
white_ru = ParagraphStyle('wr', fontName='DV-I', fontSize=14, leading=19, textColor=C('#FFD479'))
white_sm = ParagraphStyle('ws', fontName='DV', fontSize=10.5, leading=15, textColor=colors.white)
story += [sp(20), Paragraph('Conflict and Resolution', white_big), sp(4),
          Paragraph('Конфликт и его разрешение', white_ru), sp(8),
          Paragraph('Colourful Handbook  |  Unit 6A  Clear the air', white_sm),
          Paragraph('Цветной справочник  |  Раздел 6A  Разрядить обстановку', ParagraphStyle('x', parent=white_sm, textColor=C('#9fd8ff'))),
          sp(18)]
ph_w = 7.6 * cm
photo = Image('/tmp/workspace/handbook/assets/cover.jpg', width=ph_w, height=ph_w * ih / iw)
items = Table([[photo, [
    Paragraph('<b>Inside / Внутри:</b>', ParagraphStyle('i', parent=white_sm, fontSize=13, leading=18, textColor=C('#FFD479'))), sp(6),
    Paragraph('<font color="#FF8C94"><b>1.</b></font> Speaking, page 54: answers to every question', white_sm),
    Paragraph('Говорение, стр. 54: ответы на все вопросы', ParagraphStyle('r', parent=white_sm, fontName='DV-I', fontSize=8.5, textColor=C('#9fd8ff'))), sp(8),
    Paragraph('<font color="#4FE0C0"><b>2.</b></font> Defending and justifying', white_sm),
    Paragraph('Защита и оправдание', ParagraphStyle('r', parent=white_sm, fontName='DV-I', fontSize=8.5, textColor=C('#9fd8ff'))), sp(8),
    Paragraph('<font color="#D9A3F0"><b>3.</b></font> Ellipsis and substitution', white_sm),
    Paragraph('Эллипсис и замена', ParagraphStyle('r', parent=white_sm, fontName='DV-I', fontSize=8.5, textColor=C('#9fd8ff'))), sp(8),
    Paragraph('<font color="#FFD479"><b>+</b></font> Mini-quizzes, mistakes, cheat sheet', white_sm),
    Paragraph('Мини-тесты, ошибки, шпаргалка', ParagraphStyle('r', parent=white_sm, fontName='DV-I', fontSize=8.5, textColor=C('#9fd8ff'))),
    sp(8),
    Paragraph('Every English sentence has a Russian translation underneath.', white_sm),
    Paragraph('Под каждым английским предложением - перевод на русский.', ParagraphStyle('r', parent=white_sm, fontName='DV-I', fontSize=8.5, textColor=C('#9fd8ff')))]]],
    colWidths=[ph_w + 0.6 * cm, W - ph_w - 0.6 * cm])
items.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ('LEFTPADDING', (0, 0), (-1, -1), 0)]))
story += [items]
story += [NextPageTemplate('toc'), PageBreak()]

# ---- contents / how to use ----
story += [banner('How to use this handbook', 'Read, say the sentences aloud, then cover the Russian line and try again.',
                 'Как пользоваться: читайте, произносите вслух, затем закройте русскую строку и попробуйте снова.', NAVY), sp(10)]
toc = [
    ('PART 1', 'Speaking, page 54', 'Говорение, стр. 54', 'Exercise 1 (3 questions), Exercise 2 (14 topics), Exercise 3', CORAL),
    ('PART 2', 'Defending and justifying', 'Защита и оправдание', 'It\'s not as though / It\'s just that, 8 scenes, Exercise 8, quiz', TEAL),
    ('PART 3', 'Ellipsis and substitution', 'Эллипсис и замена', 'Auxiliaries, Exercises 10-12, 8 tools, mistakes, quiz, cheat sheet', PURPLE),
]
for tag, t, tr, d, col in toc:
    a = Paragraph('<font color="white"><b>%s</b></font>' % tag, ParagraphStyle('tg', fontName='DV-B', fontSize=11, alignment=TA_CENTER, leading=14))
    b = [Paragraph('<b>%s</b>' % t, ParagraphStyle('tb', parent=EN, fontSize=12, leading=16, textColor=C(NAVY))),
         Paragraph(tr, RU), Paragraph(d, EN_S)]
    r = Table([[a, b]], colWidths=[2.6 * cm, W - 2.6 * cm])
    r.setStyle(TableStyle([('BACKGROUND', (0, 0), (0, 0), C(col)), ('BACKGROUND', (1, 0), (1, 0), C('#f6f6fb')),
                           ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('LEFTPADDING', (1, 0), (1, 0), 10),
                           ('TOPPADDING', (0, 0), (-1, -1), 8), ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
                           ('ROUNDEDCORNERS', [9, 9, 9, 9])]))
    story += [r, sp(8)]
story += [sp(4), box([
    Paragraph('<b>Study tips / Советы по учёбе</b>', ParagraphStyle('t', parent=EN, textColor=C(ORANGE))),
    *pair('1. Read the English aloud. 2. Check the Russian line. 3. Cover the Russian and translate it back. 4. Make your own sentences with your own life.',
          '1. Читайте английский вслух. 2. Проверьте русскую строку. 3. Закройте перевод и переведите обратно. 4. Составляйте свои предложения о своей жизни.', True),
    *pair('Note: the answers in Part 1 are model answers. Your opinion may be different, and that is fine!',
          'Примечание: ответы в части 1 - образцы. Ваше мнение может отличаться, и это нормально!', True)], YELLOW_L, ORANGE)]
story += [NextPageTemplate('p1'), PageBreak()]
story += part1.build()
story += [NextPageTemplate('p2'), PageBreak()]
story += part2.build()
story += [NextPageTemplate('p3'), PageBreak()]
story += part3.build()
doc.build(story)
print('done')
Editing File

~/handbook/src/part3.py

Successfully replaced 1 instance(s) of the string in '/tmp/workspace/handbook/src/part3.py'
Editing File

~/handbook/src/part3.py

Successfully replaced 1 instance(s) of the string in '/tmp/workspace/handbook/src/part3.py'
Running Command

cd /tmp/workspace/handbook/src && python3 build.py 2>&1 | tail -20 && python3 -c " import pypdf r=pypdf.PdfReader('/tmp/workspace/handbook/output/Conflict_and_Resolution_Handbook.pdf');print(len(r.pages)) for i,p in enumerate(r.pages): print(i+1,len(p.extract_text()))"

Running Command

cd /tmp/workspace/handbook/src && python3 - <<'E' p=open('part1.py').read() p=p.replace("s.append(pair('<b>My answer:</b> I think the three worst","s.extend(pair('<b>My answer:</b> I think the three worst") p=p.replace("<b>родственники мужа / жены</b>.')[0:2])","<b>родственники мужа / жены</b>.'))") open('part1.py','w').write(p) E python3 build.py 2>&1 | tail -8 && python3 -c " import pypdf r=pypdf.PdfReader('/tmp/workspace/handbook/output/Conflict_and_Resolution_Handbook.pdf');print(len(r.pages)) for i,p in enumerate(r.pages): print(i+1,len(p.extract_text()))"

Running Command

cd /tmp/workspace/handbook && python3 -c " import fitz d=fitz.open('output/Conflict_and_Resolution_Handbook.pdf') for i in [0,1,2,4,12,15,19]: d[i].get_pixmap(dpi=60).save(f'assets/pg{i+1}.png') print('ok')" 2>&1 | tail -3; python3 -c "import pypdfium2" 2>&1 | tail -1

Running Command

cd /tmp/workspace/handbook && python3 -c " import pypdfium2 as pdfium d=pdfium.PdfDocument('output/Conflict_and_Resolution_Handbook.pdf') for i in [0,2,4,12,15,19]: d[i].render(scale=0.85).to_pil().save(f'assets/pg{i+1}.png') print('ok')"

Reading File
Reading File
Reading File
Reading File
Reading File
Running Command

cd /tmp/workspace/handbook/src && python3 - <<'E' p=open('part1.py').read() a=" s.append(ep('<b>What they might be saying / Что они могут говорить:</b>', ParagraphStyle('t', parent=EN, textColor=C(CORAL))))\n s.append(sp(2))\n s.append(dialogue([" b=" s.append(CondPageBreak(7 * cm))\n s.append(ep('<b>What they might be saying / Что они могут говорить:</b>', ParagraphStyle('t', parent=EN, textColor=C(CORAL))))\n s.append(sp(2))\n s.append(dialogue([" assert a in p open('part1.py','w').write(p.replace(a,b)) E python3 build.py && cd .. && python3 -c " import pypdfium2 as pdfium d=pdfium.PdfDocument('output/Conflict_and_Resolution_Handbook.pdf') print(len(d)) for i in [3,5,6,15,18]: d[i].render(scale=0.85).to_pil().save(f'assets/q{i+1}.png')"

Reading File
Reading File
Reading File
Reading File
Reading File
Your handbook is ready: a 20-page colourful PDF with a Russian translation under every English sentence.

Conflict and Resolution Handbook

PDF Document · PDF

The sample answers are my own, written to match the photo and caption. Change the details to your own opinions before using them in class. I checked the layout on only a few pages. A few pages (such as 13 and 16) end with a lot of empty space.
What's inside
  • Cover and contents: the unit photo, a contents page, and study tips.
  • Part 1, Speaking (page 54): an answer for every question.
    • Exercise 1, question 1: who the people are to each other, with three reasons from the photo.
    • Exercise 1, question 2: what they might be arguing about, with example quotes for each man.
    • Exercise 1, question 3: all six statements, each with a verdict (agree or disagree), a reason and an example.
    • Exercise 2: all 14 topics, each with how it leads to arguments and what might be said.
    • Exercise 3: the three worst areas (money, religion, in-laws) with reasons, plus a bonus tip.
  • Part 2, Defending and justifying: explained as two tools, a "shield" (It's not as though / as if…) and an "explanation card" (It's just that…).
    • Teaching material: a 3-step recipe, simple grammar notes, 10 shield sentences and 10 card sentences.
    • Practice: 8 mini-scenes, a common-mistakes table, sample answers for Exercise 8 (page 57), and a 6-question quiz with answers.
  • Part 3, Ellipsis and substitution: explained with a "text message" idea (ghost words) and a "remote-control button" idea (the auxiliary verb).
    • Book exercises: the Grammar box analysed (Exercise 10) and Exercise 11 answered with the function of each sentence.
    • Extra tools: eight more tools, such as so/not, one/ones, so do I / neither do I, comparisons and everyday spoken shortcuts.
    • Practice: a common-mistakes table, four dialogues for Exercise 12, an 8-question quiz with answers, and a one-page cheat sheet.
Judgement calls
  • Beyond page 54: you asked for page 54 answers only. I added Exercises 8, 10, 11 and 12 as bonus practice for the two grammar topics.
  • Exercise 11, sentence 4: the book wants one word per gap, so I answered "did" and "was". "I was going to" is also correct if the gap allows more words.
  • Photo: I took it from your attached PDF and used it on the cover.
This is a shared conversation. Sign in to Orris to start your own chat.