Break points after a slash or a hyphen

A justified column set twice: file paths and compounds running past the edge with no break points, then wrapping where a break after a slash or a hyphen is allowed.

Python write_break_after.py 142 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""Sets one justified paragraph in a narrow column twice: above with no break
points inside its words, below breaking after a slash or a hyphen, so the paths
and compounds that ran past the edge can be seen wrapping in it.

The Python twin of the `write_break_after` example in Rust. The text carries
file paths and hyphenated compounds whose tokens are wider than the column. The
upper setting breaks only at spaces, so those tokens run past the column's right
edge; the lower one breaks after "/" and "-", keeping the character on the line,
so the tokens wrap inside the column.

The sentence and the two headings are held in `Words`, once per language, and
`HQF_PDF_LANG` picks which set is drawn. The two paths are addresses, and stand the same
in every language.

Usage: python examples/write_break_after.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_break_after.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The two addresses the sentence carries, each wider than the column so that a break
# after a slash can be seen falling inside them.
RENDER_PATH = "/api/v2/organizations/documents/render"
PREVIEW_PATH = "/api/v2/organizations/documents/preview"


@dataclass(frozen=True)
class Words:
    """Every word the page draws, in one language.

    What is not language stays out of it: the two paths are addresses a caller types,
    and each set of words carries a hyphenated compound of its own so that both settings
    have something to break in.
    """

    # The heading over the setting that breaks at spaces only.
    plain: str
    # The heading over the setting that breaks after a slash and a hyphen.
    breaking: str
    # The sentence, cut where the two paths go into it.
    text_before: str
    text_between: str
    text_after: str

    def text(self) -> str:
        """Return the sentence both settings are broken to, with the paths in it."""
        return (
            f"{self.text_before}{RENDER_PATH}"
            f"{self.text_between}{PREVIEW_PATH}{self.text_after}"
        )


# The page in English.
ENGLISH = Words(
    plain="Without break points: the paths run past the column's edge.",
    breaking="Breaking after slash and hyphen: the paths and compounds wrap.",
    text_before="Send each request to ",
    text_between=", then read the read-only copy from ",
    text_after=" on the rate-limited tier.",
)

# The page in French.
FRENCH = Words(
    plain="Sans point de coupure : les chemins débordent de la colonne.",
    breaking="Coupure après la barre oblique et le trait d'union : tout rentre.",
    text_before="Envoyez chaque requête à ",
    text_between=", puis relisez la copie en lecture seule depuis ",
    text_after=" sur l'offre en libre-service.",
)


# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}

# The column both settings are broken to, narrow enough that the paths reach its edge.
COLUMN = 150.0

# The left edge of both settings, stacked one above the other.
X = 80.0


def main() -> None:
    language = _language.from_environment()
    words = _language.words_of(WORDS, language)
    text = words.text()

    # A named file is written as named; the default one carries the language, so the two
    # languages do not overwrite each other in `tmp/`.
    out = _out.output_path(Path(_language.file_name("break_after.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    content = hqf_pdf.Content()

    # Both panels set the same text; only whether a slash or a hyphen is a break point
    # differs between them.
    for top, label, break_after in (
        (760.0, words.plain, ""),
        (600.0, words.breaking, "/-"),
    ):
        heading = hqf_pdf.TextFlow(handle, 8.0)
        heading_lines = heading.break_lines(label, 400.0)
        heading.draw(content, heading_lines, X, top, 400.0)

        text_top = top - 20.0
        flow = hqf_pdf.TextFlow(
            handle, 11.0, leading=15.0, align=hqf_pdf.Align.Justify, break_after=break_after
        )
        lines = flow.break_lines(text, COLUMN)
        flow.draw(content, lines, X, text_top, COLUMN)

        # The column's edge, so the lines can be read against it.
        height = flow.height(lines)
        content.save_state()
        content.set_stroke(hqf_pdf.Rgb.gray(0.8))
        content.set_line_width(0.5)
        content.rect(X, text_top - height, COLUMN, height)
        content.stroke()
        content.restore_state()

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes")


if __name__ == "__main__":
    main()