123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- # -*- coding: utf-8 -*-
- # manpage/document.py
- # Part of ‘manpage’, a Python library for making Unix manual documents.
- #
- # Copyright © 2016 Ben Finney <ben+python@benfinney.id.au>
- #
- # This is free software: see the grant of license at end of this file.
- """ Structure and markup of Unix manual page documents.
- The Unix manual system is structured into documents, each called a
- “manual page”. Manual pages each belong to a topical manual
- section. Each manual section is part of a manual. There is always
- one default manual, and there may be more on a system.
- See the documentation for manual pages (on GNU+Linux, the
- ‘man-pages(7)’ page; on BSD, the ‘manpages(5)’ page) for a detailed
- explanation of writing manual page documents.
- """
- import collections
- import datetime
- import functools
- import re
- import textwrap
- from types import SimpleNamespace
- MetaData = collections.namedtuple(
- 'MetaData', "name whatis manual section source")
- class Document:
- """ A specific document (a “man page”) in the manual.
- Data attributes:
- * `metadata`: The `MetaData` instance to specify this manual page:
- * `name`: The name of this document.
- * `whatis`: The succinct one-line description of this document.
- * `manual`: The title of the manual to which this document
- belongs.
- * `section`: The section code to which this document belongs.
- * `source`: The project that includes of the item documented
- in this document.
- * `date`: The creation date of this document, as a
- `datetime.date` instance.
- * `header`: The `DocumentHeader` instance of this document.
- """
- standard_section_titles = (
- "NAME",
- "SYNOPSIS",
- "DESCRIPTION",
- "SEE ALSO",
- )
- def __init__(self, metadata):
- self.metadata = metadata
- self._created_date = datetime.date.today()
- self.header = DocumentHeader(self)
- self.content_sections = collections.OrderedDict(
- (title, None)
- for title in self.standard_section_titles)
- @property
- def date(self):
- return self._created_date
- def as_markup(self, encoding="utf-8"):
- """ Get the complete document content with markup. """
- content = self.header.as_markup(encoding)
- content += "".join(
- "{empty}\n{section}".format(
- empty=GroffMarkup.control.empty,
- section=section.as_markup())
- for section in self.content_sections.values()
- if section is not None)
- editor_hints = GroffMarkup.editor_hints(encoding)
- content += "{empty}\n{hints}".format(
- empty=GroffMarkup.control.empty,
- hints=editor_hints)
- return content
- def insert_section(self, index, section):
- """ Insert the document section at the specified index.
- :param index: The index (integer) in the existing sequence
- at which to insert this section.
- :param section: The `DocumentSection` instance to insert.
- :return: ``None``.
- """
- ordered_titles = list(self.content_sections.keys())
- ordered_titles.insert(index, section.title)
- self.content_sections[section.title] = section
- mapping_type = type(self.content_sections)
- self.content_sections = mapping_type(
- (title, self.content_sections[title])
- for title in ordered_titles)
- TitleFields = collections.namedtuple(
- 'TitleFields', "title section date source manual")
- class DocumentHeader:
- """ The header of a “man page” document.
- Data attributes:
- * `document`: The document of which this is the header.
- """
- def __init__(self, document):
- self.document = document
- @property
- def metadata(self):
- return self.document.metadata
- def title_markup(self):
- """ Get the document title as Groff markup. """
- fields = TitleFields(
- title=GroffMarkup.escapetext(
- self.metadata.name.upper(),
- hyphen=GroffMarkup.glyph.minus),
- section=GroffMarkup.escapetext(self.metadata.section),
- date=GroffMarkup.escapetext(
- self.document.date.strftime("%Y-%m-%d"),
- hyphen=GroffMarkup.glyph.minus),
- source=None,
- manual=None,
- )
- if self.metadata.source is not None:
- fields = fields._replace(
- source=GroffMarkup.escapetext(self.metadata.source))
- if self.metadata.manual is not None:
- fields = fields._replace(
- manual=GroffMarkup.escapetext(self.metadata.manual))
- result = GroffMarkup.title_command(fields)
- return result
- def as_markup(self, encoding):
- """ Get the complete document header with markup. """
- content = self.title_markup()
- return content
- class DocumentSection:
- """ A titled section in a “man page” document.
- Data attributes:
- * `title`: The title of this section, as plain text.
- * `body`: The body of the section, as marked-up text.
- """
- def __init__(self, title, body=None):
- self.title = title
- self.body = body
- def as_markup(self):
- """ Get the complete document section with markup. """
- text = textwrap.dedent("""\
- {macro.section} {section.title}
- """).format(macro=GroffMarkup.macro, section=self)
- if self.body is not None:
- text += self.body
- if not text.endswith("\n"):
- text += "\n"
- return text
- class CommandDocument(Document):
- """ A specific document in the manual of commands.
- Commands are documented with particular conventions in the
- Unix manual system.
- Data attributes:
- * `metadata`: The `MetaData` instance to specify this manual page:
- * `name`: The command documented by this manual page.
- * `whatis`: Phrasal one-line summary for the command.
- * `manual`: If unspecified, the manual system will infer the
- default title for the section code.
- * `section`: Most commands should have their manual page in
- section “1” (User commands) or “8” (System management
- commands).
- """
- standard_section_titles = (
- "NAME",
- "SYNOPSIS",
- "DESCRIPTION",
- "OPTIONS",
- "EXIT STATUS",
- "ENVIRONMENT",
- "FILES",
- "CONFORMING TO",
- "NOTES",
- "BUGS",
- "EXAMPLE",
- "SEE ALSO",
- )
- def __init__(self, metadata):
- metadata_fields = metadata._asdict()
- if metadata_fields['section'] is None:
- metadata_fields['section'] = "1"
- metadata = MetaData(**metadata_fields)
- super().__init__(metadata)
- @functools.total_ordering
- class Reference:
- """ A reference to another document. """
- def as_markup(self):
- raise NotImplementedError
- @property
- def _comparison_tuple(self):
- """ Tuple of this object used for comparison operations. """
- raise NotImplementedError
- def __eq__(self, other):
- result = False
- if isinstance(other, type(self)):
- if self._comparison_tuple == other._comparison_tuple:
- result = True
- return result
- def __lt__(self, other):
- result = False
- if isinstance(other, type(self)):
- if self._comparison_tuple < other._comparison_tuple:
- result = True
- return result
- class DocumentReference(Reference):
- """ A reference to a “man page” document in the manual.
- Data attributes:
- * `name`: The name of the document.
- * `section`: The section in the manual.
- """
- spec_pattern = re.compile(r"(?P<name>.+)\((?P<section>\d[^)]*)\)")
- class ReferenceFormatError(ValueError):
- """ Raised when parsing a malformed man page reference. """
- def __init__(self, name, section):
- self.name = name
- self.section = section
- def __str__(self):
- text = "{self.name} ({self.section})".format(self=self)
- return text
- def __repr__(self):
- class_name = self.__class__.__name__
- class_args_text = "{self.name!r}, {self.section!r}".format(self=self)
- text = "{class_name}({args})".format(
- class_name=class_name, args=class_args_text)
- return text
- @property
- def _comparison_tuple(self):
- return (self.name, self.section)
- def __lt__(self, other):
- result = super().__lt__(other)
- if isinstance(other, ExternalReference):
- # Reference to any manual page compares earlier than externals.
- result = True
- return result
- @classmethod
- def from_text(cls, text):
- """ Parse `text` to generate an instance. """
- spec_match = cls.spec_pattern.match(text)
- if spec_match is None:
- raise cls.ReferenceFormatError(text)
- reference = cls(
- name=spec_match.group('name'),
- section=spec_match.group('section'))
- return reference
- def as_markup(self):
- """ Get the reference with document markup. """
- markup = textwrap.dedent("""\
- {macro.bold_roman} {ref.name} ({ref.section})
- """).format(macro=GroffMarkup.macro, ref=self)
- return markup
- class ExternalReference(Reference):
- """ A reference to an external document.
- Data attributes:
- * `title`: The title of the document.
- * `url`: The URL to the document.
- """
- def __init__(self, title, url=None):
- self.title = title
- self.url = url
- def __str__(self):
- text_template = "{self.title}"
- if self.url is not None:
- text_template = "{self.title} <URL:{self.url}>"
- text = text_template.format(self=self)
- return text
- def __repr__(self):
- class_name = self.__class__.__name__
- class_args_text = "{self.title!r}, {self.url!r}".format(self=self)
- text = "{class_name}({args})".format(
- class_name=class_name, args=class_args_text)
- return text
- @property
- def _comparison_tuple(self):
- return (self.title, self.url)
- def as_markup(self):
- """ Get the reference with document markup. """
- title_markup = GroffMarkup.escapetext(self.title)
- if self.url is None:
- url_markup = None
- markup_template = textwrap.dedent("""\
- {title}
- """)
- else:
- url_markup = GroffMarkup.escapetext(self.url)
- markup_template = textwrap.dedent("""\
- {macro.url_begin} {url}
- {title}
- {macro.url_end}
- """)
- markup = markup_template.format(
- macro=GroffMarkup.macro,
- title=title_markup, url=url_markup)
- return markup
- class GroffMarkup:
- """ Implementation of GNU troff markup. """
- control = SimpleNamespace(
- empty=".", comment=".\\\"",
- )
- glyph = SimpleNamespace(
- backslash="\\[rs]", hyphen="\\[hy]", minus="\\-",
- registered="\\*[R]", trademark="\\*[Tm]",
- dquote_left="\\[lq]", dquote_right="\\[rq]",
- )
- font = SimpleNamespace(
- previous="\\fP", roman="\\fR", bold="\\fB", italic="\\fI")
- size = SimpleNamespace(
- normal="\\s0", decrease="\\s-1", increase="\\s+1")
- macro = SimpleNamespace(
- line_break=".br",
- title=".TH", section=".SH", subsection=".SS",
- url_begin=".UR", url_end=".UE",
- roman=".R", roman_bold=".RB", roman_italic=".RI",
- bold=".B", bold_italic=".BI", bold_roman=".BR",
- italic=".I", italic_bold=".IB", italic_roman=".IR",
- )
- @classmethod
- def encoding_declaration(cls, encoding):
- """ Make an encoding declaration line for the document. """
- text = textwrap.dedent("""\
- {comment} -*- coding: {encoding} -*-
- """).format(
- comment=cls.control.comment,
- encoding=encoding)
- return text
- @classmethod
- def editor_hints(cls, encoding):
- """ Make a comment block of editor hints. """
- text = textwrap.dedent("""\
- {comment} Local variables:
- {comment} coding: {encoding}
- {comment} mode: {syntax}
- {comment} End:
- {comment} vim: fileencoding={encoding} filetype={syntax} :
- """).format(
- comment=cls.control.comment,
- encoding=encoding, syntax="nroff")
- return text
- @classmethod
- def escapetext(cls, text, hyphen=glyph.hyphen):
- """ Replace special glyphs in `text` with appropriate markup.
- :param text: The raw input text.
- :param hyphen: The glyph to substitute for a raw hyphen.
- """
- result = text
- result = result.replace("\\", cls.glyph.backslash)
- result = result.replace("-", hyphen)
- return result
- @classmethod
- def title_command(cls, fields):
- """ Make the document title command.
- :param fields: An instance of `TitleFields` specifying the
- fields of the title command.
- :return: The generated title command line.
- """
- fields_markup = " ".join(
- '"' + field + '"'
- for field in (
- getattr(fields, name) for name in TitleFields._fields)
- if field is not None)
- result = textwrap.dedent("""\
- {macro.title} {fields}
- """).format(macro=cls.macro, fields=fields_markup)
- return result
- class ManPageMaker:
- """ Maker for a manual page document.
- Data attributes:
- * `metadata`: A `MetaData` instance specifying the document
- metadata for the manual page document.
- * `seealso`: A collection of `Reference` instances. If not
- ``None``, this is used to populate the “SEE ALSO” section of
- the document.
- """
- document_class = Document
- def __init__(self, metadata):
- self.metadata = metadata
- self.seealso = None
- def make_manpage(self):
- """ Make a manual page document from the known metadata. """
- manpage = self.document_class(self.metadata)
- manpage.content_sections.update({
- "NAME": self.make_name_section(),
- "SYNOPSIS": self.make_synopsis_section(),
- "DESCRIPTION": self.make_description_section(),
- "SEE ALSO": self.make_seealso_section(),
- })
- return manpage
- def make_name_section(self):
- """ Make the “NAME” section of the document. """
- section = DocumentSection("NAME")
- name_markup = GroffMarkup.escapetext(
- self.metadata.name, hyphen=GroffMarkup.glyph.minus)
- whatis_markup = GroffMarkup.escapetext(self.metadata.whatis)
- summary_markup = "{name} {dash} {whatis}".format(
- name=name_markup,
- dash=GroffMarkup.glyph.minus,
- whatis=whatis_markup)
- section.body = textwrap.dedent("""\
- {summary}
- """).format(summary=summary_markup)
- return section
- def make_synopsis_section(self, text=None):
- """ Make the “SYNOPSIS” section of the document. """
- section = None
- if text is not None:
- section = DocumentSection("SYNOPSIS")
- text_markup = text.rstrip()
- section.body = textwrap.dedent("""\
- {synopsis}
- """).format(synopsis=text_markup)
- return section
- def make_description_section(self, text=None):
- """ Make the “DESCRIPTION” section of the document. """
- section = None
- if text is not None:
- section = DocumentSection("DESCRIPTION")
- description_markup = GroffMarkup.escapetext(text.rstrip())
- section.body = textwrap.dedent("""\
- {description}
- """).format(description=description_markup)
- return section
- def make_seealso_section(self, references=None):
- """ Make the “SEE ALSO” section of the document. """
- section = None
- if references is None:
- references = self.seealso
- if references:
- section = DocumentSection("SEE ALSO")
- references_sorted = sorted(references)
- seealso_items = [
- reference.as_markup().rstrip()
- for reference in references_sorted]
- for (index, item) in enumerate(seealso_items[:-1]):
- if item.endswith(GroffMarkup.macro.url_end):
- item += " ,"
- else:
- item += ","
- seealso_items[index] = item
- references_markup = "\n".join(seealso_items)
- section.body = textwrap.dedent("""\
- {references}
- """).format(references=references_markup)
- return section
- class Writer:
- """ An output file writer for a “man page” document. """
- def __init__(self, document, path, encoding="utf-8"):
- self.document = document
- self.path = path
- self.encoding = encoding
- def write(self):
- """ Emit the marked-up document to the output path. """
- with open(self.path, 'w', encoding=self.encoding) as outfile:
- content = self.document.as_markup(encoding=self.encoding)
- outfile.write(content)
- # This is free software: you may copy, modify, and/or distribute this work
- # under the terms of the GNU General Public License as published by the
- # Free Software Foundation; version 3 of that license or any later version.
- #
- # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details,
- # or view it online at <URL:https://www.gnu.org/licenses/gpl-3.0.html>.
- # Local variables:
- # coding: utf-8
- # mode: python
- # End:
- # vim: fileencoding=utf-8 filetype=python :
|