File: generate_man_pages.py

package info (click to toggle)
httpie 3.2.4-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,904 kB
  • sloc: python: 13,760; xml: 162; makefile: 141; ruby: 79; sh: 32
file content (188 lines) | stat: -rw-r--r-- 6,042 bytes parent folder | download | duplicates (2)
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import os
import re
from contextlib import contextmanager
from pathlib import Path
from typing import Optional, Iterator, Iterable


# So that httpie.cli.definition can provide man-page-specific output. Must be set before importing httpie.
os.environ['HTTPIE_BUILDING_MAN_PAGES'] = '1'

import httpie
from httpie.cli.definition import options as core_options, IS_MAN_PAGE
from httpie.cli.options import ParserSpec
from httpie.manager.cli import options as manager_options
from httpie.output.ui.rich_help import OptionsHighlighter, to_usage
from httpie.output.ui.rich_utils import render_as_string


assert IS_MAN_PAGE, 'CLI definition does not understand we’re building man pages'

# Escape certain characters, so they are rendered properly on all terminals.
# <https://man7.org/linux/man-pages/man7/groff_char.7.html>
ESCAPE_MAP = {
    '"': '\[dq]',
    "'": '\[aq]',
    '~': '\(ti',
    '’': "\(ga",
    '\\': '\e',
}
ESCAPE_MAP = {ord(key): value for key, value in ESCAPE_MAP.items()}

EXTRAS_DIR = Path(__file__).parent.parent
MAN_PAGE_PATH = EXTRAS_DIR / 'man'
PROJECT_ROOT = EXTRAS_DIR.parent

OPTION_HIGHLIGHT_RE = re.compile(
    OptionsHighlighter.highlights[0]
)


class ManPageBuilder:
    def __init__(self):
        self.source = []

    def title_line(
        self,
        full_name: str,
        program_name: str,
        program_version: str,
        last_edit_date: str,
    ) -> None:
        self.source.append(
            f'.TH {program_name} 1 "{last_edit_date}" '
            f'"{full_name} {program_version}" "{full_name} Manual"'
        )

    def set_name(self, program_name: str) -> None:
        with self.section('NAME'):
            self.write(program_name)

    def write(self, text: str, *, bold: bool = False) -> None:
        if bold:
            text = '.B ' + text
        self.source.append(text)

    def separate(self) -> None:
        self.source.append('.PP')

    def format_desc(self, desc: str) -> str:
        description = _escape_and_dedent(desc)
        description = OPTION_HIGHLIGHT_RE.sub(
            # Boldify the option part, but don't remove the prefix (start of the match).
            lambda match: match[1] + self.boldify(match['option']),
            description
        )
        return description

    def add_comment(self, comment: str) -> None:
        self.source.append(f'.\\" {comment}')

    def add_options(self, options: Iterable[str], *, metavar: Optional[str] = None) -> None:
        text = ", ".join(map(self.boldify, options))
        if metavar:
            text += f' {self.underline(metavar)}'
        self.write(f'.IP "{text}"')

    def build(self) -> str:
        return '\n'.join(self.source)

    @contextmanager
    def section(self, section_name: str) -> Iterator[None]:
        self.write(f'.SH {section_name}')
        self.in_section = True
        yield
        self.in_section = False

    def underline(self, text: str) -> str:
        return r'\fI\,{}\/\fR'.format(text)

    def boldify(self, text: str) -> str:
        return r'\fB\,{}\/\fR'.format(text)


def _escape_and_dedent(text: str) -> str:
    lines = []
    for should_act, line in enumerate(text.splitlines()):
        # Only dedent after the first line.
        if should_act:
            if line.startswith('    '):
                line = line[4:]

        lines.append(line)
    return '\n'.join(lines).translate(ESCAPE_MAP)


def to_man_page(program_name: str, spec: ParserSpec, *, is_top_level_cmd: bool = False) -> str:
    builder = ManPageBuilder()
    builder.add_comment(
        f"This file is auto-generated from the parser declaration "
        + (f"in {Path(spec.source_file).relative_to(PROJECT_ROOT)} " if spec.source_file else "")
        + f"by {Path(__file__).relative_to(PROJECT_ROOT)}."
    )

    builder.title_line(
        full_name='HTTPie',
        program_name=program_name,
        program_version=httpie.__version__,
        last_edit_date=httpie.__date__,
    )
    builder.set_name(program_name)

    with builder.section('SYNOPSIS'):
        # `http` and `https` are commands that can be directly used, so they can have
        # a valid usage. But `httpie` is a top-level command with multiple sub commands,
        # so for the synopsis we'll only reference the `httpie` name.
        if is_top_level_cmd:
            synopsis = program_name
        else:
            synopsis = render_as_string(to_usage(spec, program_name=program_name))
        builder.write(synopsis)

    with builder.section('DESCRIPTION'):
        builder.write(spec.description)
        if spec.man_page_hint:
            builder.write(spec.man_page_hint)

    for index, group in enumerate(spec.groups, 1):
        with builder.section(group.name):
            if group.description:
                builder.write(group.description)

            for argument in group.arguments:
                if argument.is_hidden:
                    continue

                raw_arg = argument.serialize(isolation_mode=True)

                metavar = raw_arg.get('metavar')
                if raw_arg.get('is_positional'):
                    # In case of positional arguments, metavar is always equal
                    # to the list of options (e.g `METHOD`).
                    metavar = None
                builder.add_options(raw_arg['options'], metavar=metavar)

                desc = builder.format_desc(raw_arg.get('description', ''))
                builder.write('\n' + desc + '\n')

            builder.separate()

    if spec.epilog:
        with builder.section('SEE ALSO'):
            builder.write(builder.format_desc(spec.epilog))

    return builder.build()


def main() -> None:
    for program_name, spec, config in [
        ('http', core_options, {}),
        ('https', core_options, {}),
        ('httpie', manager_options, {'is_top_level_cmd': True}),
    ]:
        with open((MAN_PAGE_PATH / program_name).with_suffix('.1'), 'w') as stream:
            stream.write(to_man_page(program_name, spec, **config))


if __name__ == '__main__':
    main()