From 80bd53a33142458784bd10f6b768483d5c870f3a Mon Sep 17 00:00:00 2001 From: Brecht Machiels Date: Fri, 21 Jun 2024 01:26:53 +0200 Subject: [PATCH] Refactor localized and user-defined strings - collect all localized string in Language - remove StringCollection and subclasses - the sections in a template file corresponding to the string collections are replaced by the [STRINGS] section - differentiate between builtin/user-defined strings by means of the @/$ symbols preceding the string identifiers (e.g. in [STRINGS]) - localize more strings: paragraph, section, listing --- CHANGES.rst | 7 +- doc/api/strings.rst | 12 -- doc/api/structure.rst | 4 - doc/basicstyling.rst | 9 +- doc/my_book.rtt | 10 +- doc/rinohtype.rtt | 4 +- src/rinoh/__init__.py | 6 - src/rinoh/document.py | 35 +++--- src/rinoh/flowable.py | 3 +- src/rinoh/highlight.py | 2 +- src/rinoh/image.py | 11 +- src/rinoh/index.py | 6 +- src/rinoh/language/__init__.py | 8 +- src/rinoh/language/cls.py | 33 +++-- src/rinoh/language/cs.py | 14 +-- src/rinoh/language/de.py | 14 +-- src/rinoh/language/en.py | 19 ++- src/rinoh/language/es.py | 13 +- src/rinoh/language/fr.py | 14 +-- src/rinoh/language/it.py | 14 +-- src/rinoh/language/nl.py | 19 ++- src/rinoh/language/pl.py | 14 +-- src/rinoh/paragraph.py | 7 +- src/rinoh/reference.py | 10 +- src/rinoh/strings.py | 139 +++++----------------- src/rinoh/structure.py | 37 +----- src/rinoh/template.py | 10 +- src/rinoh/templates/book.py | 6 +- src/rinoh/text.py | 3 +- tests/test_parse.py | 15 ++- tests_regression/rst/stringfield.rtt | 21 ++-- tests_regression/rst/stringfield_lang.rtt | 9 +- 32 files changed, 173 insertions(+), 355 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fc9002eae..1fce98845 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -41,6 +41,11 @@ New Features: Changed: +* handling of localized strings and user-defined strings has been reworked + * built-in (localized) strings are now indicated by a ``@`` prefix, while + user-defined strings have the ``$`` prefix + * built-in strings can be overridden, and user-defined strings can be set in + a template configuration file in the ``[STRINGS]`` section (with prefix!) * "words" containing spaces (such as paths and URLs) can now be split before each forward slash for line wrapping (#188, #416) * Support for Python 3.7 was dropped (end-of-life in June 2023) @@ -61,7 +66,7 @@ Changed: Fixed: -* Caption labels ("Figure" ad "Table") were not localized +* Caption labels ("Figure", "Table", "Listing") were not localized * Rendering of tables with no body (#420, PR #422 by th0mr) * Hyphenation of the first word on a line (#188, #416) * AttributeError: 'ZeroWidthSpace' object has no attribute 'hyphenate' (#415, diff --git a/doc/api/strings.rst b/doc/api/strings.rst index 97ce86a0b..981bd7e83 100644 --- a/doc/api/strings.rst +++ b/doc/api/strings.rst @@ -5,18 +5,6 @@ Strings (:mod:`rinoh.strings`) ============================== -.. autoclass:: String - :members: - - -.. autoclass:: StringCollection - :members: - - -.. autoclass:: UserStrings - :members: - - .. autoclass:: Strings :members: diff --git a/doc/api/structure.rst b/doc/api/structure.rst index 4c765c3ca..c4069e975 100644 --- a/doc/api/structure.rst +++ b/doc/api/structure.rst @@ -17,10 +17,6 @@ Sections :members: -.. autoclass:: SectionTitles - :members: - - Lists ~~~~~ diff --git a/doc/basicstyling.rst b/doc/basicstyling.rst index c8ba02055..e1531d543 100644 --- a/doc/basicstyling.rst +++ b/doc/basicstyling.rst @@ -167,11 +167,10 @@ distinction is only visible when using multiple columns for the page contents The standard document strings configured by the :attr:`~.DocumentTemplate.language` option described above can be overridden by -user-defined strings in the :class:`~.SectionTitles` and -:class:`~.AdmonitionTitles` sections of the configuration file. For example, -the default title for the table of contents section (*Table of Contents*) is -replaced with *Contents*. The configuration also sets custom titles for the -caution and warning admonitions. +user-defined strings in the ``STRINGS`` section of the configuration file. For +example, the default title for the table of contents section (*Table of +Contents*) is replaced with *Contents*. The configuration also sets custom +titles for the caution and warning admonitions. The others sections in the configuration file are the ``VARIABLES`` section, followed by document part and page template sections. Similar to style sheets, diff --git a/doc/my_book.rtt b/doc/my_book.rtt index 54672826e..07bf6dc10 100644 --- a/doc/my_book.rtt +++ b/doc/my_book.rtt @@ -10,12 +10,10 @@ parts = stylesheet = sphinx_base14 language = fr -[SectionTitles] -contents = 'Contents' - -[AdmonitionTitles] -caution = 'Careful!' -warning = 'Please be warned' +[STRINGS] +@contents = 'Contents' +@caution = 'Careful!' +@warning = 'Please be warned' [VARIABLES] paper_size = A5 diff --git a/doc/rinohtype.rtt b/doc/rinohtype.rtt index 75fbd097b..a45a5d6d6 100644 --- a/doc/rinohtype.rtt +++ b/doc/rinohtype.rtt @@ -11,8 +11,8 @@ parts = stylesheet = rinohtype.rts -[SectionTitles] -contents='Contents' +[STRINGS] +@contents='Contents' [VARIABLES] diff --git a/src/rinoh/__init__.py b/src/rinoh/__init__.py index af9887911..4c2bd5599 100644 --- a/src/rinoh/__init__.py +++ b/src/rinoh/__init__.py @@ -50,9 +50,3 @@ register_template = resource._DISTRIBUTION.register_template register_typeface = resource._DISTRIBUTION.register_typeface - -# list all StringCollection subclasses in its docstring -_ = ['* :class:`.{}`'.format(subclass_name) - for subclass_name in sorted(strings.StringCollection.subclasses)] -strings.StringCollection.__doc__ += ('\n\n :Subclasses: ' - + '\n '.join(_)) diff --git a/src/rinoh/document.py b/src/rinoh/document.py index e2a0036f5..79f5deb16 100644 --- a/src/rinoh/document.py +++ b/src/rinoh/document.py @@ -231,7 +231,8 @@ class Document(object): document_tree (DocumentTree): a tree of the document's contents stylesheet (StyleSheet): style sheet used to style document elements language (Language): the language to use for standard strings - strings (Strings): overrides localized strings provided by `language` + strings (Strings): user-defined string variables and can override + localized strings provided by `language` backend: the backend used for rendering the document """ @@ -380,21 +381,27 @@ def _save_cache(self, filename): cache = (self.part_page_counts, self.page_references) pickle.dump(cache, file) - def set_string(self, strings_class, key, value): - self._strings[strings_class][key] = value - - def get_string(self, strings_class, key): - if (strings_class in self._strings - and key in self._strings[strings_class]): - return self._strings[strings_class][key] + def set_string(self, key, value, user=False): + if user: + self._strings.set_user_string(key, value) + else: + self._strings.set_builtin_string(key, value) + + def get_string(self, key, user=False): + if user: + result = self._strings.user.get(key, None) + if result is None: + warn('The "{}" user string is not defined.') + return result or '' + if key in self._strings.builtin: + return self._strings.builtin[key] try: - return self.language.strings[strings_class][key] + return self.language.strings[key] except KeyError: - warn('The {} "{}" string is not defined for {} ({}). Using the ' - 'English string instead.'.format(strings_class.__name__, key, - self.language.name, - self.language.code)) - return EN.strings[strings_class][key] + warn('The "{}" string is not defined for {} ({}). Using the English' + ' string instead.' + .format(key, self.language.name, self.language.code)) + return EN.strings[key] def add_sideways_float(self, float): self.sideways_floats.append(float) diff --git a/src/rinoh/flowable.py b/src/rinoh/flowable.py index 51d7e53c2..9f5dfaeff 100644 --- a/src/rinoh/flowable.py +++ b/src/rinoh/flowable.py @@ -28,7 +28,6 @@ from .layout import (InlineDownExpandingContainer, VirtualContainer, MaybeContainer, ContainerOverflow, EndOfContainer, PageBreakException, ReflowRequired) -from .strings import UserStrings from .style import Styled, Style from .text import StyledText from .util import clamp, ReadAliasAttribute @@ -431,7 +430,7 @@ def __init__(self, label, content, parent=None): def build_document(self, flowable_target): doc = flowable_target.document - doc.set_string(UserStrings, self.label, self.content) + doc.set_string(self.label, self.content, user=True) class SetOutOfLineFlowables(DummyFlowable): diff --git a/src/rinoh/highlight.py b/src/rinoh/highlight.py index 79ba95278..3807e70f7 100644 --- a/src/rinoh/highlight.py +++ b/src/rinoh/highlight.py @@ -59,7 +59,7 @@ class CodeBlockWithCaptionStyle(FloatStyle, GroupedFlowablesStyle): class CodeBlockWithCaption(Float, StaticGroupedFlowables): style_class = CodeBlockWithCaptionStyle - category = 'Listing' + category = 'listing' def highlight_block(language, text, lexer_getter): diff --git a/src/rinoh/image.py b/src/rinoh/image.py index aca45984d..bdc9be48c 100644 --- a/src/rinoh/image.py +++ b/src/rinoh/image.py @@ -23,7 +23,7 @@ from .layout import ContainerOverflow, EndOfContainer from .number import NumberFormat from .paragraph import StaticParagraph, Paragraph, ParagraphStyle -from .strings import StringCollection, String, StringField +from .strings import StringField from .structure import ListOf, ListOfSection from .text import MixedStyledText, SingleStyledText, TextStyle, ErrorText from .util import posix_path, ReadAliasAttribute, PeekIterator @@ -398,7 +398,7 @@ def referenceable(self): def text(self, container): try: number = self.number(container) - category_label = StringField(FloatLabels, self.referenceable.category) + category_label = StringField(self.referenceable.category) label = [category_label, ' ', number] except KeyError: label = [] @@ -420,10 +420,3 @@ class ListOfFigures(ListOf): class ListOfFiguresSection(ListOfSection): list_class = ListOfFigures - - -class FloatLabels(StringCollection): - """Collection of localized titles for common sections""" - - figure = String('Caption label for figures') - table = String('Caption label for tables') diff --git a/src/rinoh/index.py b/src/rinoh/index.py index 31362a578..fdffac9a2 100644 --- a/src/rinoh/index.py +++ b/src/rinoh/index.py @@ -11,7 +11,7 @@ from .paragraph import Paragraph from .reference import Reference from .strings import StringField -from .structure import Section, Heading, SectionTitles +from .structure import Section, Heading from .style import Styled from .text import MixedStyledText, StyledText from .util import intersperse @@ -23,7 +23,7 @@ class IndexSection(Section): def __init__(self, title=None, flowables=None, style=None): - section_title = title or StringField(SectionTitles, 'index') + section_title = title or StringField('index') contents = [Heading(section_title, style='unnumbered')] if flowables: contents += list(flowables) @@ -127,7 +127,7 @@ def spans(self, container): class IndexTarget(IndexTargetBase, DummyFlowable): - category = 'Index' + category = 'index' def __init__(self, index_terms, parent=None): super().__init__(index_terms, parent=parent) diff --git a/src/rinoh/language/__init__.py b/src/rinoh/language/__init__.py index eb77f4417..9fbf45c61 100644 --- a/src/rinoh/language/__init__.py +++ b/src/rinoh/language/__init__.py @@ -26,10 +26,6 @@ for code, language_ref in Language.languages.items(): language = language_ref() lines = ['Localized strings for {}'.format(language.name)] - for string_collection in language.strings.values(): - lines.append("\n.. rubric:: {}\n" - .format(type(string_collection).__name__)) - for string in string_collection._strings: - lines.append(":{}: {}".format(string.name, - string_collection[string.name])) + for name, localized_string in language.strings.items(): + lines.append(":{}: {}".format(name, localized_string)) language.__doc__ = '\n'.join(lines) diff --git a/src/rinoh/language/cls.py b/src/rinoh/language/cls.py index ccf21b4b2..77dc4b752 100644 --- a/src/rinoh/language/cls.py +++ b/src/rinoh/language/cls.py @@ -9,7 +9,6 @@ import weakref from ..attribute import AttributeType -from ..strings import StringCollection __all__ = ['Language'] @@ -22,27 +21,43 @@ class Language(AttributeType): code (str): short code identifying the language name (str): native name of the language + paragraph: label for referencing paragraphs + section: label for referencing sections + chapter: label for top-level sections + figure: caption label for figures + table: caption label for tables + listing: caption label for (source code) listings + contents: title for the table of contents section + list_of_figures: title for the list of figures section + list_of_tables: title for the list of tables section + index: title for the index section + + attention: title for attention admonitions + caution: title for caution admonitions + danger: title for danger admonitions + error: title for error admonitions + hint: title for hint admonitions + important: title for important admonitions + note: title for note admonitions + tip: title for tip admonitions + warning: title for warning admonitions + seealso: title for see-also admonitions + """ languages = {} #: Dictionary mapping codes to :class:`Language`\ s - def __init__(self, code, name): + def __init__(self, code, name, **localized_strings): self.languages[code] = weakref.ref(self) self.code = code self.name = name - self.strings = {} + self.strings = localized_strings self.no_break_after = [] def __repr__(self): return "{}('{}', '{}')".format(type(self).__name__, self.code, self.name) - def __contains__(self, item): - assert isinstance(item, StringCollection) - strings_class = type(item) - assert strings_class not in self.strings - self.strings[strings_class] = item - @classmethod def parse_string(cls, string, source): return cls.languages[string.lower()]() diff --git a/src/rinoh/language/cs.py b/src/rinoh/language/cs.py index 687854c7f..5cc97a6b1 100644 --- a/src/rinoh/language/cs.py +++ b/src/rinoh/language/cs.py @@ -7,26 +7,18 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -CS = Language('cs', 'Česky') - -FloatLabels( +CS = Language('cs', 'Česky', figure='Obrázek', table='Tabulka', -) in CS - -SectionTitles( contents='Obsah', list_of_figures='Seznam obrázků', list_of_tables='Seznam tabulek', chapter='Kapitola', index='Rejstřík', -) in CS -AdmonitionTitles( + # admonitions attention='Pozor!', caution='Pozor!', danger='!NEBEZPEČÍ!', @@ -37,7 +29,7 @@ tip='Tip', warning='Varování', seealso='Viz také', -) in CS +) CS.no_break_after = ("do od u z ze za k ke o na v ve nad pod za po s se " diff --git a/src/rinoh/language/de.py b/src/rinoh/language/de.py index 6fbc15130..8fae41de9 100644 --- a/src/rinoh/language/de.py +++ b/src/rinoh/language/de.py @@ -7,26 +7,18 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -DE = Language('de', 'Deutsch') - -FloatLabels( +DE = Language('de', 'Deutsch', figure='Abbildung', table='Tabelle', -) in DE - -SectionTitles( contents='Inhalt', list_of_figures='Abbildungsverzeichnis', list_of_tables='Tabellenverzeichnis', chapter='Kapitel', index='Index', -) in DE -AdmonitionTitles( + # admonitions attention='Aufgepasst!', caution='Vorsicht!', danger='!GEFAHR!', @@ -37,4 +29,4 @@ tip='Tipp', warning='Warnung', seealso='Siehe auch', -) in DE +) diff --git a/src/rinoh/language/en.py b/src/rinoh/language/en.py index 509535f89..bc8793f7a 100644 --- a/src/rinoh/language/en.py +++ b/src/rinoh/language/en.py @@ -7,26 +7,21 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -EN = Language('en', 'English') - -FloatLabels( +EN = Language('en', 'English', + paragraph='Paragraph', + section='Section', + chapter='Chapter', figure='Figure', table='Table', -) in EN - -SectionTitles( + listing='Listing', contents='Table of Contents', list_of_figures='List of Figures', list_of_tables='List of Tables', - chapter='Chapter', index='Index', -) in EN -AdmonitionTitles( + # admonitions attention='Attention!', caution='Caution!', danger='!DANGER!', @@ -37,6 +32,6 @@ tip='Tip', warning='Warning', seealso='See also', -) in EN +) EN.no_break_after = "a an the".split() diff --git a/src/rinoh/language/es.py b/src/rinoh/language/es.py index f2ec09acf..6f76497b9 100644 --- a/src/rinoh/language/es.py +++ b/src/rinoh/language/es.py @@ -7,25 +7,18 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -ES = Language('es', 'Spanish') -FloatLabels( +ES = Language('es', 'Spanish', figure='Figura', table='Tabla', -) in ES - -SectionTitles( contents='Contenidos', list_of_figures='Índice de figuras', list_of_tables='Índice de tablas', chapter='Capítulo', index='Índice', -) in ES -AdmonitionTitles( + # admonitions attention='¡Atención!', caution='¡Cuidado!', danger='¡PELIGRO!', @@ -36,4 +29,4 @@ tip='Consejo', warning='Precaución', seealso='Vea también', -) in ES +) diff --git a/src/rinoh/language/fr.py b/src/rinoh/language/fr.py index b926d64d1..4ebad5c93 100644 --- a/src/rinoh/language/fr.py +++ b/src/rinoh/language/fr.py @@ -7,26 +7,18 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -FR = Language('fr', 'Français') - -FloatLabels( +FR = Language('fr', 'Français', figure='Figure', table='Tableau', -) in FR - -SectionTitles( contents='Table des Matières', list_of_figures='Liste des Figures', list_of_tables='Liste des Tableaux', chapter='Chapitre', index='Index', -) in FR -AdmonitionTitles( + # admonitions attention='Attention!', caution='Prudence!', danger='!DANGER!', @@ -37,4 +29,4 @@ tip='Astuce', warning='Avertissement', seealso='Voir aussi', -) in FR +) diff --git a/src/rinoh/language/it.py b/src/rinoh/language/it.py index 488541ef1..1a803efd5 100644 --- a/src/rinoh/language/it.py +++ b/src/rinoh/language/it.py @@ -7,26 +7,18 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -IT = Language('it', 'Italiano') - -FloatLabels( +IT = Language('it', 'Italiano', figure='Figura', table='Tabelle', -) in IT - -SectionTitles( contents='Contenuti', list_of_figures='Elenco delle Figure', list_of_tables='Elenco delle Tabelle', chapter='Capitolo', index='Indice', -) in IT -AdmonitionTitles( + # admonitions attention='Attenzione!', caution='Prudenza!', danger='!PERICOLO!', @@ -37,4 +29,4 @@ tip='Suggerimento', warning='Avvertimento', seealso='Vedi anche', -) in IT +) diff --git a/src/rinoh/language/nl.py b/src/rinoh/language/nl.py index 78f817315..dfaf82ffa 100644 --- a/src/rinoh/language/nl.py +++ b/src/rinoh/language/nl.py @@ -7,26 +7,21 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -NL = Language('nl', 'Nederlands') - -FloatLabels( +NL = Language('nl', 'Nederlands', + paragraph='Paragraaf', + section='Sectie', + chapter='Hoofdstuk', figure='Figuur', table='Tabel', -) in NL - -SectionTitles( + listing='Lijst', contents='Inhoudsopgave', list_of_figures='Lijst van Figuren', list_of_tables='Lijst van Tabellen', - chapter='Hoofdstuk', index='Index', -) in NL -AdmonitionTitles( + # admonitions attention='Opgelet!', caution='Pas op!', danger='!GEVAAR!', @@ -37,4 +32,4 @@ tip='Tip', warning='Waarschuwing', seealso='Zie ook', -) in NL +) diff --git a/src/rinoh/language/pl.py b/src/rinoh/language/pl.py index 2e27e34c9..528ba5b0f 100644 --- a/src/rinoh/language/pl.py +++ b/src/rinoh/language/pl.py @@ -7,26 +7,18 @@ from .cls import Language -from ..image import FloatLabels -from ..structure import SectionTitles, AdmonitionTitles -PL = Language('pl', 'Polski') - -FloatLabels( +PL = Language('pl', 'Polski', figure='Ilustracja', table='Tabela', -) in PL - -SectionTitles( contents='Spis Treści', list_of_figures='Spis Ilustracji', list_of_tables='Spis Tabel', chapter='Rozdział', index='Skorowidz', -) in PL -AdmonitionTitles( + # admonitions attention='Uwaga!', caution='Ostrożnie!', danger='!NIEBEZPIECZEŃSTWO!', @@ -37,4 +29,4 @@ tip='Porada', warning='Ostrzeżenie', seealso='Zobacz również', -) in PL +) diff --git a/src/rinoh/paragraph.py b/src/rinoh/paragraph.py index 215c303c9..5c31be619 100644 --- a/src/rinoh/paragraph.py +++ b/src/rinoh/paragraph.py @@ -31,6 +31,7 @@ from .inline import InlineFlowableException from .layout import EndOfContainer, ContainerOverflow from .number import NumberStyle, Label, format_number +from .strings import StringField from .text import (TextStyle, StyledText, SingleStyledText, MixedStyledText, ESCAPE, LANGUAGE_DEFAULT) from .util import all_subclasses, ReadAliasAttribute, consumer @@ -671,7 +672,7 @@ class ParagraphBase(Flowable, Label): """ - category = 'Paragraph' + category = 'paragraph' style_class = ParagraphStyle significant_whitespace = False @@ -708,8 +709,8 @@ def prepare(self, flowable_target): label = self.prepare_label(number, section, flowable_target) else: return - category = self.referenceable.category - reference = MixedStyledText([category, ' ', label]) + category_label = StringField(self.referenceable.category) + reference = MixedStyledText([category_label, ' ', label]) for id in self.referenceable.get_ids(document): document.set_reference(id, 'number', label) document.set_reference(id, 'reference', reference) diff --git a/src/rinoh/reference.py b/src/rinoh/reference.py index 1fc36742d..426992e76 100644 --- a/src/rinoh/reference.py +++ b/src/rinoh/reference.py @@ -16,7 +16,7 @@ from .layout import ContainerOverflow from .number import NumberStyle, Label, format_number from .paragraph import Paragraph, ParagraphStyle, ParagraphBase -from .strings import StringCollection, StringField +from .strings import StringField from .style import HasClass, HasClasses, Styled from .text import (StyledText, TextStyle, SingleStyledText, MixedStyledTextBase, MixedStyledText, ErrorText) @@ -224,7 +224,7 @@ class NoteStyle(LabeledFlowableStyle): class Note(LabeledFlowable): - category = 'Note' + category = 'note' style_class = NoteStyle def __init__(self, flowable, id=None, style=None, parent=None): @@ -369,11 +369,7 @@ class SECTION_TITLE(SectionFieldType): reference_type = 'title' -from . import structure # fills StringCollection.subclasses - -RE_STRINGFIELD = ('|'.join(r'{}\.(?:[a-z_][a-z0-9_]*)' - .format(collection_name) - for collection_name in StringCollection.subclasses)) +RE_STRINGFIELD = r'[@$](?:[a-z_][a-z0-9_]*)' class Field(MixedStyledTextBase): diff --git a/src/rinoh/strings.py b/src/rinoh/strings.py index 1dae4dc9e..393fd24a4 100644 --- a/src/rinoh/strings.py +++ b/src/rinoh/strings.py @@ -13,112 +13,36 @@ from .util import NamedDescriptor, WithNamedDescriptors -__all__ = ['String', 'StringCollection', 'Strings', 'StringField'] +__all__ = ['Strings', 'StringField'] -class String(NamedDescriptor): - """Descriptor used to describe a configurable string - - Args: - description (str): a short description for this string - - """ - - def __init__(self, description): - self.description = description - self.name = None - - def __get__(self, strings, type=None): - try: - return strings.get(self.name) - except AttributeError: - return self - - def __set__(self, strings, value): - if not StyledText.check_type(value): - raise TypeError('String attributes only accept styled text' - .format(self.name)) - strings[self.name] = value - - -class StringCollectionMeta(WithNamedDescriptors): - def __new__(metacls, classname, bases, cls_dict): - cls = super().__new__(metacls, classname, bases, cls_dict) - try: - StringCollection.subclasses[classname] = cls - except NameError: - pass # cls is StringCollection - else: - strings = [] - attrs = [] - for name, string in cls_dict.items(): - if not isinstance(string, String): - continue - strings.append(string) - attrs.append('{} (:class:`.{}`): {}' - .format(name, type(string).__name__, - string.description)) - cls._strings = strings - cls.__doc__ += ('\n ' - .join(chain(['\n\n Attributes:'], attrs))) - return cls - - -class StringCollection(dict, metaclass=StringCollectionMeta): - """A collection of related configurable strings""" - - subclasses = {} - - def __init__(self, **strings): - for name, value in strings.items(): - string_descriptor = getattr(type(self), name, None) - if not isinstance(string_descriptor, String): - raise AttributeError("'{}' is not an accepted string for {}" - .format(name, type(self).__name__)) - setattr(self, name, value) - - def __getitem__(self, name): - return getattr(self, name) - - -class UserStrings(dict, metaclass=StringCollectionMeta): - """Collection of user-specified strings - - Unlike other string collections, these are not limited to a predefined set. - - """ - - def __init__(self, **strings): - for name, value in strings.items(): - if not StyledText.check_type(value): - raise TypeError('String attributes only accept styled text' - .format(self.name)) - self[name] = value - - -class Strings(AcceptNoneAttributeType, dict): +class Strings(AcceptNoneAttributeType): """Stores several :class:`StringCollection`\\ s""" - def __init__(self, *string_collections): - for string_collection in string_collections: - self[type(string_collection)] = string_collection + def __init__(self): + self.builtin = {} + self.user = {} + + def __setitem__(self, identifier, value): + symbol = identifier[0] + if symbol not in '@$': + raise ValueError("A string identifier need to start with @, for " + "builtin strings, or $ for user-defined strings") + key = identifier[1:] + if symbol == '@': + self.set_builtin_string(key, value) + else: + self.set_user_string(key, value) - def __setitem__(self, string_collection_class, string_collection): - if string_collection_class in self: - raise ValueError("{} is already registered with this {} instance" - .format(string_collection_class.__name__, - type(self).__name__)) - super().__setitem__(string_collection_class, string_collection) + def set_builtin_string(self, key, value): + self.builtin[key] = value - def __missing__(self, string_collection_class): - instance = string_collection_class() - self[string_collection_class] = instance - return instance + def set_user_string(self, key, value): + self.user[key] = value @classmethod def doc_format(cls): - return ('strings need to be entered in INI sections named after the ' - ':class:`.StringCollection` subclasses') + return "strings need to be entered in INI in a section named 'STRINGS'" class StringField(MixedStyledTextBase): @@ -129,37 +53,34 @@ class StringField(MixedStyledTextBase): :class:`.TemplateConfiguration`. """ - def __init__(self, strings_class, key, - style=None, parent=None, source=None): + def __init__(self, key, style=None, parent=None, source=None, user=False): super().__init__(style=style, parent=parent, source=source) - self.strings_class = strings_class self.key = key + self.user = user def __eq__(self, other): return type(self) == type(other) and self.__dict__ == other.__dict__ def __str__(self): - result = "'{{{}.{}}}'".format(self.strings_class.__name__, self.key) + result = "'{{{}{}}}'".format('$' if self.user else '@', self.key) if self.style is not None: result += ' ({})'.format(self.style) return result def __repr__(self): - return ("{}({}, '{}', style={})" - .format(type(self).__name__, self.strings_class.__name__, - self.key, self.style)) + return ("{}({!r}, style={})" + .format(type(self).__name__, self.key, self.style)) @classmethod def parse_string(cls, string, style=None): - collection, key = string.split('.') - return cls(StringCollection.subclasses[collection], key, style=style) + return cls(string[1:], style=style, user=string[0] == '$') def copy(self, parent=None): - return type(self)(self.strings_class, self.key, style=self.style, - parent=parent, source=self.source) + return type(self)(self.key, style=self.style, parent=parent, + source=self.source, user=self.user) def children(self, container): - text = container.document.get_string(self.strings_class, self.key) + text = container.document.get_string(self.key, self.user) if isinstance(text, StyledText): text.parent = self yield text diff --git a/src/rinoh/structure.py b/src/rinoh/structure.py index bda0efbdb..e1e677f82 100644 --- a/src/rinoh/structure.py +++ b/src/rinoh/structure.py @@ -22,8 +22,7 @@ from .reference import (ReferenceField, ReferencingParagraph, ReferencingParagraphStyle) from .text import StyledText, SingleStyledText, MixedStyledText, Tab -from .style import PARENT_STYLE -from .strings import StringCollection, String, StringField +from .strings import StringField from .util import NotImplementedAttribute, itemcount __all__ = ['Section', 'Heading', @@ -36,16 +35,6 @@ 'HorizontalRule', 'HorizontalRuleStyle', 'OutOfLineFlowables'] -class SectionTitles(StringCollection): - """Collection of localized titles for common sections""" - - contents = String('Title for the table of contents section') - list_of_figures = String('Title for the list of figures section') - list_of_tables = String('Title for the list of tables section') - chapter = String('Label for top-level sections') - index = String('Title for the index section') - - class SectionStyle(GroupedFlowablesStyle): show_in_toc = Attribute(Bool, True, 'List this section in the table of ' 'contents') @@ -61,7 +50,7 @@ class SectionBase(GroupedFlowables): @property def category(self): - return 'Chapter' if self.level == 1 else 'Section' + return 'chapter' if self.level == 1 else 'section' @property def level(self): @@ -219,7 +208,7 @@ def __init__(self, base=None, **attributes): class TableOfContentsSection(Section): def __init__(self): - section_title = StringField(SectionTitles, 'contents') + section_title = StringField('contents') super().__init__([Heading(section_title, style='unnumbered'), TableOfContents()], style='table of contents') @@ -295,7 +284,7 @@ class ListOfSection(Section): def __init__(self): key = 'list_of_{}s'.format(self.list_class.category.lower()) - section_title = StringField(SectionTitles, key) + section_title = StringField(key) self.list_of = self.list_class() super().__init__([Heading(section_title, style='unnumbered'), self.list_of], @@ -384,21 +373,6 @@ class AdmonitionStyle(GroupedFlowablesStyle): "with the body text, if possible") -class AdmonitionTitles(StringCollection): - """Collection of localized titles for common admonitions""" - - attention = String('Title for attention admonitions') - caution = String('Title for caution admonitions') - danger = String('Title for danger admonitions') - error = String('Title for error admonitions') - hint = String('Title for hint admonitions') - important = String('Title for important admonitions') - note = String('Title for note admonitions') - tip = String('Title for tip admonitions') - warning = String('Title for warning admonitions') - seealso = String('Title for see-also admonitions') - - class Admonition(StaticGroupedFlowables): style_class = AdmonitionStyle @@ -413,8 +387,7 @@ def custom_title_text(self): return self.custom_title.to_string(None) if self.custom_title else None def title(self, document): - return (self.custom_title - or document.get_string(AdmonitionTitles, self.admonition_type)) + return self.custom_title or document.get_string(self.admonition_type) def flowables(self, container): title = self.title(container.document) diff --git a/src/rinoh/template.py b/src/rinoh/template.py index 9a2d0c88f..c1ec43908 100644 --- a/src/rinoh/template.py +++ b/src/rinoh/template.py @@ -36,7 +36,7 @@ PAGE_NUMBER, NUMBER_OF_PAGES) from .resource import Resource from .text import StyledText, Tab -from .strings import StringCollection, Strings +from .strings import Strings from .structure import Header, Footer, HorizontalRule, NewChapterException from .style import StyleSheet, Specificity, DocumentLocationType from .stylesheets import sphinx @@ -617,12 +617,10 @@ class TemplateConfigurationFile(RuleSetFile, TemplateConfiguration): main_section = 'TEMPLATE_CONFIGURATION' def process_section(self, section_name, classifier, items): - if section_name in StringCollection.subclasses: - collection_cls = StringCollection.subclasses[section_name] + if section_name == 'STRINGS': strings = self.setdefault('strings', Strings()) - collection_items = {name: StyledText.from_string(value) - for name, value in items} - strings[collection_cls] = collection_cls(**collection_items) + for identifier, value in items: + strings[identifier] = StyledText.from_string(value) else: template_class = self.get_entry_class(section_name) self[section_name] = template_class(**dict(items)) diff --git a/src/rinoh/templates/book.py b/src/rinoh/templates/book.py index 450982efc..9abe8dd02 100644 --- a/src/rinoh/templates/book.py +++ b/src/rinoh/templates/book.py @@ -14,7 +14,7 @@ from ..reference import (Field, PAGE_NUMBER, DOCUMENT_TITLE, DOCUMENT_SUBTITLE, SECTION_NUMBER, SECTION_TITLE) from ..strings import StringField -from ..structure import TableOfContentsSection, SectionTitles +from ..structure import TableOfContentsSection from ..stylesheets import sphinx from ..table import ListOfTablesSection from ..template import (TitlePageTemplate, BodyPageTemplate, DocumentTemplate, @@ -27,7 +27,7 @@ style='front matter section title')] -BODY_TITLE = [Paragraph(StringField(SectionTitles, 'chapter') + ' ' +BODY_TITLE = [Paragraph(StringField('chapter') + ' ' + Field(SECTION_NUMBER(1), style='number'), style='body matter chapter label'), Paragraph(Field(SECTION_TITLE(1)), @@ -117,7 +117,7 @@ class Book(DocumentTemplate): header_text=(Field(DOCUMENT_TITLE) + ', ' + Field(DOCUMENT_SUBTITLE)), footer_text=(Field(PAGE_NUMBER) + Tab() + Tab() + - StringField(SectionTitles, 'chapter') + StringField('chapter') + ' ' + Field(SECTION_NUMBER(1)) + '. ' + Field(SECTION_TITLE(1))), sideways='left') diff --git a/src/rinoh/text.py b/src/rinoh/text.py index c7e163ce5..72f353ac3 100644 --- a/src/rinoh/text.py +++ b/src/rinoh/text.py @@ -249,8 +249,7 @@ def substitute_controlchars_htmlentities(string, style=None): try: return ControlCharacter.all[string]() except KeyError: - return SingleStyledText(string.format(**NAME2CHAR), - style=style) + return SingleStyledText(string.format(**NAME2CHAR), style=style) return Field.substitute(text, substitute_controlchars_htmlentities, style=style) diff --git a/tests/test_parse.py b/tests/test_parse.py index 6077a0fc1..2559a14c1 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -23,8 +23,7 @@ ProportionalSpacing, FixedSpacing, Leading) from rinoh.reference import (ReferenceField, ReferenceText, Field, PAGE_NUMBER, NUMBER_OF_PAGES, SECTION_TITLE, SECTION_NUMBER) -from rinoh.strings import StringField, UserStrings -from rinoh.structure import SectionTitles, AdmonitionTitles +from rinoh.strings import StringField from rinoh.style import (SelectorByName, parse_selector, parse_selector_args, parse_class_selector, parse_keyword, parse_string, parse_number, StyleParseError, CharIterator) @@ -374,15 +373,15 @@ def test_styledtext_from_string_field(): Field(PAGE_NUMBER)], style='style1'), SingleStyledText('moh', style='style2')]) - assert StyledText.from_string("'{SectionTitles.chapter}abc' (style)") \ - == MixedStyledText([StringField(SectionTitles, 'chapter'), + assert StyledText.from_string("'{@chapter}abc' (style)") \ + == MixedStyledText([StringField('chapter'), SingleStyledText('abc')], style='style') - assert StyledText.from_string("'1{AdmonitionTitles.warning}2' (style)") \ + assert StyledText.from_string("'1{@warning}2' (style)") \ == MixedStyledText([SingleStyledText('1'), - StringField(AdmonitionTitles, 'warning'), + StringField('warning'), SingleStyledText('2')], style='style') - assert StyledText.from_string("'{UserStrings.my_string}abc' (style)") \ - == MixedStyledText([StringField(UserStrings, 'my_string'), + assert StyledText.from_string("'{$my_string}abc' (style)") \ + == MixedStyledText([StringField('my_string', user=True), SingleStyledText('abc')], style='style') diff --git a/tests_regression/rst/stringfield.rtt b/tests_regression/rst/stringfield.rtt index 04b250a4f..efc310f6a 100644 --- a/tests_regression/rst/stringfield.rtt +++ b/tests_regression/rst/stringfield.rtt @@ -5,20 +5,19 @@ template=book parts=contents stylesheet=sphinx_base14 -[VARIABLES] -paper_size=A5 - -[SectionTitles] -chapter='Ch.' (emphasis) +[STRINGS] +; override built-in strings +@chapter='Ch.' (emphasis) +@warning='Be warned!' -[AdmonitionTitles] -warning='Be warned!' +; user-defined strings +$my_string='my string' -[UserStrings] -my_string='my string' +[VARIABLES] +paper_size=A5 [contents_left_page] -header_text='{UserStrings.my_subst}' -footer_text='{UserStrings.my_string}' (monospaced) +header_text='{$my_subst}' +footer_text='{$my_string}' (monospaced) '\t \t {SECTION_NUMBER(1)}. {SECTION_TITLE(1)}' diff --git a/tests_regression/rst/stringfield_lang.rtt b/tests_regression/rst/stringfield_lang.rtt index 0fe4d8b23..9bb5239e8 100644 --- a/tests_regression/rst/stringfield_lang.rtt +++ b/tests_regression/rst/stringfield_lang.rtt @@ -6,13 +6,12 @@ parts=contents stylesheet=sphinx_base14 language=nl +[STRINGS] +$my_string='my string' + [VARIABLES] paper_size=A5 -[UserStrings] -my_string='my string' - [contents_left_page] -footer_text= - '{UserStrings.my_string} \t \t {SECTION_NUMBER(1)}. {SECTION_TITLE(1)}' +footer_text='{$my_string} \t \t {SECTION_NUMBER(1)}. {SECTION_TITLE(1)}'