1"""
2ustache module.
3
4This module alone implements the entire ustache library, including its
5minimal command line interface.
6
7The main functions are considered to be :func:`cli`, :func:`render` and
8:func:`stream`, and they expose different approaches to template rendering:
9command line interface, buffered and streaming rendering API, respectively.
10
11Other functionality will be considered **advanced**, exposing some
12implementation-specific complexity and potentially non-standard behavior
13which could reduce compatibility with other mustache implentations and
14future major ustache versions.
15
16"""
17"""
18ustache, Mustache for Python
19============================
20
21See `README.md`_, `project documentation`_ and `project repository`_.
22
23.. _README.md: https://ustache.readthedocs.io/en/latest/README.html
24.. _project documentation: https://ustache.readthedocs.io
25.. _project repository: https://gitlab.com/ergoithz/ustache
26
27
28License
29-------
30
31Copyright (c) 2021, Felipe A Hernandez.
32
33MIT License (see `LICENSE`_).
34
35.. _LICENSE: https://gitlab.com/ergoithz/ustache/-/blob/master/LICENSE
36
37"""
38
39import codecs
40import collections
41import collections.abc
42import functools
43import sys
44import types
45import typing
46
47try:
48    import xxhash  # type: ignore
49    _cache_hash = next(filter(callable, (
50        getattr(xxhash, name, None)
51        for name in (
52        'xxh3_64_intdigest',
53        'xxh64_intdigest',
54    )
55    )))
56    """
57    Generate template hash using the fastest algorithm available:
58
59    1. :py:func:`xxhash.xxh3_64_intdigest` from ``python-xxhash>=2.0.0``.
60    2. :py:func:`xxhash.xxh64_intdigest` from ``python-xxhash>=1.2.0``.
61
62    """
63except (ImportError, StopIteration):
64    def _cache_hash(template: bytes) -> bytes:
65        """Get template itself as hash fallback."""
66        return template
67
68__author__ = 'Felipe A Hernandez'
69__email__ = 'ergoithz@gmail.com'
70__license__ = 'MIT'
71__version__ = '0.1.5'
72__all__ = (
73    # api
74    'tokenize',
75    'stream',
76    'render',
77    'cli',
78    # exceptions
79    'TokenException',
80    'ClosingTokenException',
81    'UnclosedTokenException',
82    'DelimiterTokenException',
83    # defaults
84    'default_resolver',
85    'default_getter',
86    'default_stringify',
87    'default_escape',
88    'default_lambda_render',
89    'default_tags',
90    'default_cache',
91    'default_virtuals',
92    # types
93    'TString',
94    'PartialResolver',
95    'PropertyGetter',
96    'StringifyFunction',
97    'EscapeFunction',
98    'LambdaRenderFunctionFactory',
99    'LambdaRenderFunctionConstructor',
100    'CompiledTemplate',
101    'CompiledToken',
102    'CompiledTemplateCache',
103    'TagsTuple',
104)
105
106if sys.version_info > (3, 9):
107    _abc = collections.abc
108    _TagsTuple = tuple[typing.AnyStr, typing.AnyStr]
109    _TagsByteTuple = tuple[bytes, bytes]
110    _CompiledToken = tuple[bool, bool, bool, slice, slice, int]
111    _CompiledTemplate = tuple[_CompiledToken, ...]
112    _CacheKey = tuple[typing.Union[bytes, int], bytes, bytes, bool]
113    _TokenizeStack = list[tuple[slice, int, int, int]]
114    _TokenizeRecording = list[_CompiledToken]
115    _SiblingIterator = typing.Optional[_abc.Iterator]
116    _ProcessStack = list[tuple[_SiblingIterator, bool, bool]]
117else:
118    _abc = typing
119    _TagsTuple = typing.Tuple[typing.AnyStr, typing.AnyStr]
120    _TagsByteTuple = typing.Tuple[bytes, bytes]
121    _CompiledToken = typing.Tuple[bool, bool, bool, slice, slice, int]
122    _CompiledTemplate = typing.Tuple[_CompiledToken, ...]
123    _CacheKey = typing.Tuple[typing.Union[bytes, int], bytes, bytes, bool]
124    _TokenizeStack = typing.List[typing.Tuple[slice, int, int, int]]
125    _TokenizeRecording = typing.List[_CompiledToken]
126    _SiblingIterator = typing.Optional[_abc.Iterator]
127    _ProcessStack = typing.List[typing.Tuple[_SiblingIterator, bool, bool]]
128
129T = typing.TypeVar('T')
130"""Generic."""
131
132D = typing.TypeVar('D')
133"""Generic."""
134
135TString = typing.TypeVar('TString', str, bytes)
136"""String/bytes generic."""
137
138PartialResolver = _abc.Callable[[typing.AnyStr], typing.AnyStr]
139"""Template partial tag resolver function interface."""
140
141PropertyGetter = _abc.Callable[
142    [typing.Any, _abc.Sequence[typing.Any], typing.AnyStr, typing.Any],
143    typing.Any,
144]
145"""Template property getter function interface."""
146
147StringifyFunction = _abc.Callable[[bytes, bool], bytes]
148"""Template variable general stringification function interface."""
149
150EscapeFunction = _abc.Callable[[bytes], bytes]
151"""Template variable value escape function interface."""
152
153LambdaRenderFunctionConstructor = _abc.Callable[
154    ...,
155    _abc.Callable[..., typing.AnyStr],
156]
157"""Lambda render function constructor interface."""
158
159LambdaRenderFunctionFactory = LambdaRenderFunctionConstructor
160"""
161.. deprecated:: 0.1.3
162    Use :attr:`LambdaRenderFunctionConstructor` instead.
163
164"""
165
166VirtualPropertyFunction = _abc.Callable[[typing.Any], typing.Any]
167"""Virtual property implementation callable interface."""
168
169VirtualPropertyMapping = _abc.Mapping[str, VirtualPropertyFunction]
170"""Virtual property mapping interface."""
171
172TagsTuple = _TagsTuple
173"""Mustache tag tuple interface."""
174
175CompiledToken = _CompiledToken
176"""
177Compiled template token.
178
179Tokens are tuples containing a renderer decission path, key, content and flags.
180
181``type: bool``
182    Decission for rendering path node `a`.
183
184``type: bool``
185    Decission for rendering path node `b`.
186
187``type: bool``
188    Decission for rendering path node `c`
189
190``key: Optional[slice]``
191    Template slice for token scope key, if any.
192
193``content: Optional[slice]``
194    Template slice for token content data, if any.
195
196``flags: int``
197    Token flags.
198
199    - Unused: ``-1`` (default)
200    - Variable flags:
201        - ``0`` - escaped
202        - ``1`` - unescaped
203    - Block start flags:
204        - ``0`` - falsy
205        - ``1`` - truthy
206    - Block end value: block content index.
207
208"""
209
210CompiledTemplate = _CompiledTemplate
211"""
212Compiled template interface.
213
214.. seealso::
215
216    :py:attr:`ustache.CompiledToken`
217        Item type.
218
219    :py:attr:`ustache.CompiledTemplateCache`
220        Interface exposing this type.
221
222"""
223
224CompiledTemplateCache = _abc.MutableMapping[_CacheKey, CompiledTemplate]
225"""
226Cache mapping interface.
227
228.. seealso::
229
230    :py:attr:`ustache.CompiledTemplateCache`
231        Item type.
232
233"""
234
235
236class LRUCache(collections.OrderedDict, typing.Generic[T]):
237    """Capped mapping discarding least recently used elements."""
238
239    def __init__(self, maxsize: int, *args, **kwargs) -> None:
240        """
241        Initialize.
242
243        :param maxsize: maximum number of elements will be kept
244
245        Any parameter excess will be passed straight to dict constructor.
246
247        """
248        self.maxsize = maxsize
249        super().__init__(*args, **kwargs)
250
251    def get(self, key: _abc.Hashable, default: D = None) -> typing.Union[T, D]:
252        """
253        Get value for given key or default if not present.
254
255        :param key: hashable
256        :param default: value will be returned if key is not present
257        :returns: value if key is present, default if not
258
259        """
260        try:
261            return self[key]
262        except KeyError:
263            return default  # type: ignore
264
265    def __getitem__(self, key: _abc.Hashable) -> T:
266        """
267        Get value for given key.
268
269        :param key: hashable
270        :returns: value if key is present
271        :raises KeyError: if key is not present
272
273        """
274        self.move_to_end(key)
275        return super().__getitem__(key)
276
277    def __setitem__(self, key: _abc.Hashable, value: T) -> None:
278        """
279        Set value for given key.
280
281        :param key: hashable will be used to retrieve values later on
282        :param value: value for given key
283
284        """
285        super().__setitem__(key, value)
286        try:
287            self.move_to_end(key)
288            while len(self) > self.maxsize:
289                self.popitem(last=False)
290        except KeyError:  # race condition
291            pass
292
293
294default_tags = (b'{{', b'}}')
295"""Tuple of default mustache tags (in bytes)."""
296
297default_cache: CompiledTemplateCache = LRUCache(1024)
298"""
299Default template cache mapping, keeping the 1024 most recently used
300compiled templates (LRU expiration).
301
302If `xxhash`_ is available, template data won't be included in cache.
303
304.. _xxhash: https://pypi.org/project/xxhash/
305
306"""
307
308
309def virtual_length(ref: typing.Any) -> int:
310    """
311    Resolve virtual length property.
312
313    :param ref: any non-mapping object implementing `__len__`
314    :returns: number of items
315    :raises TypeError: if ref is mapping or has no `__len__` method
316
317    """
318    if isinstance(ref, collections.abc.Mapping):
319        raise TypeError
320    return len(ref)
321
322
323default_virtuals: VirtualPropertyMapping = types.MappingProxyType({
324    'length': virtual_length,
325})
326"""
327Immutable mapping with default virtual properties.
328
329The following virtual properties are implemented:
330
331- **length**, for non-mapping sized objects, returning ``len(ref)``.
332
333"""
334
335TOKEN_TYPES = [(True, False, True, False)] * 0x100
336TOKEN_TYPES[0x21] = False, False, False, True  # '!'
337TOKEN_TYPES[0x23] = False, True, True, False  # '#'
338TOKEN_TYPES[0x26] = True, True, True, True  # '&'
339TOKEN_TYPES[0x2F] = False, True, False, False  # '/'
340TOKEN_TYPES[0x3D] = False, False, False, False  # '='
341TOKEN_TYPES[0x3E] = False, False, True, False  # '>'
342TOKEN_TYPES[0x5E] = False, True, True, True  # '^'
343TOKEN_TYPES[0x7B] = True, True, False, True  # '{'
344"""ASCII-indexed tokenizer decission matrix."""
345
346FALSY_PRIMITIVES = None, False, 0, float('nan'), float('-nan')
347EMPTY = slice(0)
348EVERYTHING = slice(None)
349
350
351class TokenException(SyntaxError):
352    """Invalid token found during tokenization."""
353
354    message = 'Invalid tag {tag} at line {row} column {column}'
355
356    @classmethod
357    def from_template(
358            cls,
359            template: bytes,
360            start: int,
361            end: int,
362    ) -> 'TokenException':
363        """
364        Create exception instance from parsing data.
365
366        :param template: template bytes
367        :param start: character position where the offending tag starts at
368        :param end: character position where the offending tag ends at
369        :returns: exception instance
370
371        """
372        tag = template[start:end].decode()
373        row = 1 + template[:start].count(b'\n')
374        column = 1 + start - max(0, template.rfind(b'\n', 0, start))
375        return cls(cls.message.format(tag=tag, row=row, column=column))
376
377
378class ClosingTokenException(TokenException):
379    """Non-matching closing token found during tokenization."""
380
381    message = 'Non-matching tag {tag} at line {row} column {column}'
382
383
384class UnclosedTokenException(ClosingTokenException):
385    """Unclosed token found during tokenization."""
386
387    message = 'Unclosed tag {tag} at line {row} column {column}'
388
389
390class DelimiterTokenException(TokenException):
391    """
392    Invalid delimiters token found during tokenization.
393
394    .. versionadded:: 0.1.1
395
396    """
397
398    message = 'Invalid delimiters {tag} at line {row} column {column}'
399
400
401def default_stringify(
402        data: typing.Any,
403        text: bool = False,
404) -> bytes:
405    """
406    Convert arbitrary data to bytes.
407
408    :param data: value will be serialized
409    :param text: whether running in text mode or not (bytes mode)
410    :returns: template bytes
411
412    """
413    return (
414        data
415        if isinstance(data, bytes) and not text else
416        data.encode()
417        if isinstance(data, str) else
418        f'{data}'.encode()
419    )
420
421
422def replay(
423        recording: typing.Sequence[CompiledToken],
424        start: int = 0,
425) -> _abc.Generator[CompiledToken, int, None]:
426    """
427    Yield template tokenization from cached data.
428
429    This generator accepts sending back a token index, which will result on
430    rewinding it back and repeat everything from there.
431
432    :param recording: token list
433    :param start: starting index
434    :returns: token tuple generator
435
436    """
437    size = len(recording)
438    while True:
439        for item in range(start, size):
440            start = yield recording[item]
441            if start is not None:
442                break
443        else:
444            break
445
446
447def default_escape(data: bytes) -> bytes:
448    """
449    Convert bytes conflicting with HTML to their escape sequences.
450
451    :param data: bytes containing text
452    :returns: escaped text bytes
453
454    """
455    return (
456        data
457        .replace(b'&', b'&')
458        .replace(b'<', b'&lt;')
459        .replace(b'>', b'&gt;')
460        .replace(b'"', b'&quot;')
461        .replace(b'\'', b'&#x60;')
462        .replace(b'`', b'&#x3D;')
463    )
464
465
466def default_resolver(name: typing.AnyStr) -> bytes:
467    """
468    Mustache partial resolver function (stub).
469
470    :param name: partial template name
471    :returns: empty bytes
472
473    """
474    return b''
475
476
477def default_getter(
478        scope: typing.Any,
479        scopes: _abc.Sequence[typing.Any],
480        key: typing.AnyStr,
481        default: typing.Any = None,
482        *,
483        virtuals: VirtualPropertyMapping = default_virtuals,
484) -> typing.Any:
485    """
486    Extract property value from scope hierarchy.
487
488    :param scope: uppermost scope (corresponding to key ``'.'``)
489    :param scopes: parent scope sequence
490    :param key: property key
491    :param default: value will be used as default when missing
492    :param virtuals: mapping of virtual property callables
493    :return: value from scope or default
494
495    .. versionadded:: 0.1.3
496       *virtuals* parameter.
497
498    Both :class:`AttributeError` and :class:`TypeError` exceptions
499    raised by virtual property implementations will be handled as if
500    that property doesn't exist, which can be useful to filter out
501    incompatible types.
502
503    """
504    if key in (b'.', '.'):
505        return scope
506
507    binary_mode = not isinstance(key, str)
508    components = key.split(b'.' if binary_mode else '.')  # type: ignore
509    for ref in (*scopes, scope)[::-1]:
510        for name in components:
511            if binary_mode:
512                try:
513                    ref = ref[name]
514                    continue
515                except (KeyError, TypeError, AttributeError):
516                    pass
517                try:
518                    name = name.decode()  # type: ignore
519                except UnicodeDecodeError:
520                    break
521            try:
522                ref = ref[name]
523                continue
524            except (KeyError, TypeError, AttributeError):
525                pass
526            try:
527                ref = (
528                    ref[int(name)]
529                    if name.isdigit() else
530                    getattr(ref, name)  # type: ignore
531                )
532                continue
533            except (KeyError, TypeError, AttributeError, IndexError):
534                pass
535            try:
536                ref = virtuals[name](ref)  # type: ignore
537                continue
538            except (KeyError, TypeError, AttributeError):
539                pass
540            break
541        else:
542            return ref
543    return default
544
545
546def default_lambda_render(
547        scope: typing.Any,
548        **kwargs,
549) -> _abc.Callable[[TString], TString]:
550    r"""
551    Generate a template-only render function with fixed parameters.
552
553    :param scope: current scope
554    :param \**kwargs: parameters forwarded to :func:`render`
555    :returns: template render function
556
557    """
558
559    def lambda_render(template: TString) -> TString:
560        """
561        Render given template to string/bytes.
562
563        :param template: template text
564        :returns: rendered string or bytes (depending on template type)
565
566        """
567        return render(template, scope, **kwargs)
568
569    return lambda_render
570
571
572def slicestrip(template: bytes, start: int, end: int) -> slice:
573    """
574    Strip slice from whitespace on bytes.
575
576    :param template: bytes where whitespace should be stripped
577    :param start: substring slice start
578    :param end: substring slice end
579    :returns: resulting stripped slice
580
581    """
582    c = template[start:end]
583    return slice(end - len(c.lstrip()), start + len(c.rstrip()))
584
585
586def tokenize(
587        template: bytes,
588        *,
589        tags: TagsTuple = default_tags,
590        comments: bool = False,
591        cache: CompiledTemplateCache = default_cache,
592) -> _abc.Generator[CompiledToken, int, None]:
593    """
594    Generate token tuples from mustache template.
595
596    This generator accepts sending back a token index, which will result on
597    rewinding it back and repeat everything from there.
598
599    :param template: template as utf-8 encoded bytes
600    :param tags: mustache tag tuple (open, close)
601    :param comments: whether yield comment tokens or not (ignore comments)
602    :param cache: mutable mapping for compiled template cache
603    :return: token tuple generator (type, name slice, content slice, option)
604
605    :raises UnclosedTokenException: if token is left unclosed
606    :raises ClosingTokenException: if block closing token does not match
607    :raises DelimiterTokenException: if delimiter token syntax is invalid
608
609    """
610    tokenization_key = _cache_hash(template), *tags, comments  # type: ignore
611
612    cached = cache.get(tokenization_key)
613    if cached:  # recordings must contain at least one token
614        yield from replay(cached)
615        return
616
617    template_find = template.find
618
619    stack: _TokenizeStack = []
620    stack_append = stack.append
621    stack_pop = stack.pop
622    scope_label = EVERYTHING
623    scope_head = 0
624    scope_start = 0
625    scope_index = 0
626
627    empty = EMPTY
628    start_tag, end_tag = tags
629    end_literal = b'}' + end_tag
630    end_switch = b'=' + end_tag
631    start_len = len(start_tag)
632    end_len = len(end_tag)
633
634    token: CompiledToken
635    token_types = TOKEN_TYPES
636    t = slice
637    s = functools.partial(slicestrip, template)
638    recording: _TokenizeRecording = []
639    record = recording.append
640
641    text_start, text_end = 0, template_find(start_tag)
642    while text_end != -1:
643        if text_start < text_end:  # text
644            token = False, True, False, empty, t(text_start, text_end), -1
645            record(token)
646            yield token
647
648        tag_start = text_end + start_len
649        try:
650            a, b, c, d = token_types[template[tag_start]]
651        except IndexError:
652            raise UnclosedTokenException.from_template(
653                template=template,
654                start=text_end,
655                end=tag_start,
656            ) from None
657
658        if a:  # variables
659            tag_start += b
660
661            if c:  # variable
662                tag_end = template_find(end_tag, tag_start)
663                text_start = tag_end + end_len
664
665            else:  # triple-keyed variable
666                tag_end = template_find(end_literal, tag_start)
667                text_start = tag_end + end_len + 1
668
669            token = False, True, True, s(tag_start, tag_end), empty, d
670
671        elif b:  # block
672            tag_start += 1
673            tag_end = template_find(end_tag, tag_start)
674            text_start = tag_end + end_len
675
676            if c:  # open
677                stack_append((scope_label, text_end, scope_start, scope_index))
678                scope_label = s(tag_start, tag_end)
679                scope_head = text_end
680                scope_start = text_start
681                scope_index = len(recording)
682                token = True, True, False, scope_label, empty, d
683
684            elif template[scope_label] != template[tag_start:tag_end].strip():
685                raise ClosingTokenException.from_template(
686                    template=template,
687                    start=text_end,
688                    end=text_start,
689                )
690
691            else:  # close
692                token = (
693                    True, True, True,
694                    scope_label, s(scope_start, text_end), scope_index,
695                )
696                scope_label, scope_head, scope_start, scope_index = stack_pop()
697
698        elif c:  # partial
699            tag_start += 1
700            tag_end = template_find(end_tag, tag_start)
701            text_start = tag_end + end_len
702            token = True, False, True, s(tag_start, tag_end), empty, -1
703
704        elif d:  # comment
705            tag_start += 1
706            tag_end = template_find(end_tag, tag_start)
707            text_start = tag_end + end_len
708
709            if not comments:
710                text_end = template_find(start_tag, text_start)
711                continue
712
713            token = True, False, False, empty, s(tag_start, tag_end), -1
714
715        else:  # tags
716            tag_start += 1
717            tag_end = template_find(end_switch, tag_start)
718            text_start = tag_end + end_len + 1
719
720            try:
721                start_tag, end_tag = template[tag_start:tag_end].split(b' ')
722                if not (start_tag and end_tag):
723                    raise ValueError
724
725            except ValueError:
726                raise DelimiterTokenException.from_template(
727                    template=template,
728                    start=text_end,
729                    end=text_start,
730                ) from None
731
732            end_literal = b'}' + end_tag
733            end_switch = b'=' + end_tag
734            start_len = len(start_tag)
735            end_len = len(end_tag)
736            start_end = tag_start + start_len
737            end_start = tag_end - end_len
738            token = (
739                False, False, True,
740                s(tag_start, start_end), s(end_start, tag_end), -1,
741            )
742
743        if tag_end < 0:
744            raise UnclosedTokenException.from_template(
745                template=template,
746                start=tag_start,
747                end=tag_end,
748            )
749
750        record(token)
751        rewind = yield token
752        if rewind is not None:
753            yield from replay(recording, rewind)
754
755        text_end = template_find(start_tag, text_start)
756
757    if stack:
758        raise UnclosedTokenException.from_template(
759            template=template,
760            start=scope_head,
761            end=scope_start,
762        )
763
764    # end
765    token = False, False, False, empty, t(text_start, None), -1
766    cache[tokenization_key] = (*recording, token)
767    yield token
768
769
770def process(
771        template: TString,
772        scope: typing.Any,
773        *,
774        scopes: _abc.Iterable[typing.Any] = (),
775        resolver: PartialResolver = default_resolver,
776        getter: PropertyGetter = default_getter,
777        stringify: StringifyFunction = default_stringify,
778        escape: EscapeFunction = default_escape,
779        lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
780        tags: TagsTuple = default_tags,
781        cache: CompiledTemplateCache = default_cache,
782) -> _abc.Generator[bytes, int, None]:
783    """
784    Generate rendered mustache template byte chunks.
785
786    :param template: mustache template string
787    :param scope: root object used as root mustache scope
788    :param scopes: iterable of parent scopes
789    :param resolver: callable will be used to resolve partials (bytes)
790    :param getter: callable will be used to pick variables from scope
791    :param stringify: callable will be used to render python types (bytes)
792    :param escape: callable will be used to escape template (bytes)
793    :param lambda_render: lambda render function constructor
794    :param tags: mustache tag tuple (open, close)
795    :param cache: mutable mapping for compiled template cache
796    :return: byte chunk generator
797
798    :raises UnclosedTokenException: if token is left unclosed
799    :raises ClosingTokenException: if block closing token does not match
800    :raises DelimiterTokenException: if delimiter token syntax is invalid
801
802    """
803    text_mode = isinstance(template, str)
804
805    # encoding
806    data: bytes = (
807        template.encode()  # type: ignore
808        if text_mode else
809        template
810    )
811    current_tags: _TagsByteTuple = tuple(  # type: ignore
812        tag.encode() if isinstance(tag, str) else tag
813        for tag in tags[:2]
814    )
815
816    # current context
817    siblings: _SiblingIterator = None
818    callback = False
819    silent = False
820
821    # context stack
822    stack: _ProcessStack = []
823    stack_append = stack.append
824    stack_pop = stack.pop
825
826    # scope stack
827    scopes = list(scopes)
828    scopes_append = scopes.append
829    scopes_pop = scopes.pop
830
831    # locals
832    read = data.__getitem__
833    decode = (
834        (lambda x: data[x].decode())  # type: ignore
835        if text_mode else
836        read
837    )
838    missing = object()
839    falsy_primitives = FALSY_PRIMITIVES
840    TIterable = collections.abc.Iterable
841    TNonLooping = (
842        str,
843        bytes,
844        collections.abc.Mapping,
845    )
846    TSequence = collections.abc.Sequence
847
848    tokens = tokenize(data, tags=current_tags, cache=cache)
849    rewind = tokens.send
850    for a, b, c, token_name, token_content, token_option in tokens:
851        if silent:
852            if a and b:
853                if c:  # closing/loop
854                    closing_scope = scope
855                    closing_callback = callback
856
857                    scope = scopes_pop()
858                    siblings, callback, silent = stack_pop()
859
860                    if closing_callback and not silent:
861                        yield stringify(
862                            closing_scope(
863                                decode(token_content),
864                                lambda_render(
865                                    scope=scope,
866                                    scopes=scopes,
867                                    resolver=resolver,
868                                    escape=escape,
869                                    tags=current_tags,
870                                    cache=cache,
871                                ),
872                            ),
873                            text_mode,
874                        )
875
876                else:  # block
877                    scopes_append(scope)
878                    stack_append((siblings, callback, silent))
879        elif a:
880            if b:
881                if c:  # closing/loop
882                    if siblings:
883                        try:
884                            scope = next(siblings)
885                            callback = callable(scope)
886                            silent = callback
887                            rewind(token_option)
888                        except StopIteration:
889                            scope = scopes_pop()
890                            siblings, callback, silent = stack_pop()
891                    else:
892                        scope = scopes_pop()
893                        siblings, callback, silent = stack_pop()
894
895                else:  # block
896                    scopes_append(scope)
897                    stack_append((siblings, callback, silent))
898
899                    scope = getter(scope, scopes, decode(token_name), None)
900                    falsy = (  # emulate JS falseness
901                            scope in falsy_primitives
902                            or isinstance(scope, TSequence) and not scope
903                    )
904                    if token_option:  # falsy block
905                        siblings = None
906                        callback = False
907                        silent = not falsy
908                    elif falsy:  # truthy block with falsy value
909                        siblings = None
910                        callback = False
911                        silent = True
912                    elif (
913                            isinstance(scope, TIterable)
914                            and not isinstance(scope, TNonLooping)
915                    ):  # loop block
916                        try:
917                            siblings = iter(scope)
918                            scope = next(siblings)
919                            callback = callable(scope)
920                            silent = callback
921                        except StopIteration:
922                            siblings = None
923                            scope = None
924                            callback = False
925                            silent = True
926                    else:  # truthy block with truthy value
927                        siblings = None
928                        callback = callable(scope)
929                        silent = callback
930
931            elif c:  # partial
932                value = resolver(decode(token_name))
933                if value:
934                    yield from process(
935                        template=value,
936                        scope=scope,
937                        scopes=scopes,
938                        resolver=resolver,
939                        escape=escape,
940                        tags=current_tags,
941                        cache=cache,
942                    )
943            # else: comment
944        elif b:
945            if c:  # variable
946                value = getter(scope, scopes, decode(token_name), missing)
947                if value is not missing:
948                    yield (
949                        stringify(value, text_mode)
950                        if token_option else
951                        escape(stringify(value, text_mode))
952                    )
953
954            else:  # text
955                yield read(token_content)
956
957        elif c:  # tags
958            current_tags = read(token_name), read(token_content)
959
960        else:  # end
961            yield read(token_content)
962
963
964def stream(
965        template: TString,
966        scope: typing.Any,
967        *,
968        scopes: _abc.Iterable[typing.Any] = (),
969        resolver: PartialResolver = default_resolver,
970        getter: PropertyGetter = default_getter,
971        stringify: StringifyFunction = default_stringify,
972        escape: EscapeFunction = default_escape,
973        lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
974        tags: TagsTuple = default_tags,
975        cache: CompiledTemplateCache = default_cache,
976) -> _abc.Generator[TString, None, None]:
977    """
978    Generate rendered mustache template chunks.
979
980    :param template: mustache template (str or bytes)
981    :param scope: current rendering scope (data object)
982    :param scopes: list of precedent scopes
983    :param resolver: callable will be used to resolve partials (bytes)
984    :param getter: callable will be used to pick variables from scope
985    :param stringify: callable will be used to render python types (bytes)
986    :param escape: callable will be used to escape template (bytes)
987    :param lambda_render: lambda render function constructor
988    :param tags: tuple (start, end) specifying the initial mustache delimiters
989    :param cache: mutable mapping for compiled template cache
990    :returns: generator of bytes/str chunks (same type as template)
991
992    :raises UnclosedTokenException: if token is left unclosed
993    :raises ClosingTokenException: if block closing token does not match
994    :raises DelimiterTokenException: if delimiter token syntax is invalid
995
996    """
997    chunks = process(
998        template=template,
999        scope=scope,
1000        scopes=scopes,
1001        resolver=resolver,
1002        getter=getter,
1003        stringify=stringify,
1004        escape=escape,
1005        lambda_render=lambda_render,
1006        tags=tags,
1007        cache=cache,
1008    )
1009    yield from (
1010        codecs.iterdecode(chunks, 'utf8')
1011        if isinstance(template, str) else
1012        chunks
1013    )
1014
1015
1016def render(
1017        template: TString,
1018        scope: typing.Any,
1019        *,
1020        scopes: _abc.Iterable[typing.Any] = (),
1021        resolver: PartialResolver = default_resolver,
1022        getter: PropertyGetter = default_getter,
1023        stringify: StringifyFunction = default_stringify,
1024        escape: EscapeFunction = default_escape,
1025        lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
1026        tags: TagsTuple = default_tags,
1027        cache: CompiledTemplateCache = default_cache,
1028) -> TString:
1029    """
1030    Render mustache template.
1031
1032    :param template: mustache template
1033    :param scope: current rendering scope (data object)
1034    :param scopes: list of precedent scopes
1035    :param resolver: callable will be used to resolve partials (bytes)
1036    :param getter: callable will be used to pick variables from scope
1037    :param stringify: callable will be used to render python types (bytes)
1038    :param escape: callable will be used to escape template (bytes)
1039    :param lambda_render: lambda render function constructor
1040    :param tags: tuple (start, end) specifying the initial mustache delimiters
1041    :param cache: mutable mapping for compiled template cache
1042    :returns: rendered bytes/str (type depends on template)
1043
1044    :raises UnclosedTokenException: if token is left unclosed
1045    :raises ClosingTokenException: if block closing token does not match
1046    :raises DelimiterTokenException: if delimiter token syntax is invalid
1047
1048    """
1049    data = b''.join(process(
1050        template=template,
1051        scope=scope,
1052        scopes=scopes,
1053        resolver=resolver,
1054        getter=getter,
1055        stringify=stringify,
1056        escape=escape,
1057        lambda_render=lambda_render,
1058        tags=tags,
1059        cache=cache,
1060    ))
1061    return data.decode() if isinstance(template, str) else data
1062
1063
1064def cli(argv: typing.Optional[_abc.Sequence[str]] = None) -> None:
1065    """
1066    Render template from command line.
1067
1068    Use `python -m ustache --help` to check available options.
1069
1070    :param argv: command line arguments, :attr:`sys.argv` when None
1071
1072    """
1073    import argparse
1074    import json
1075    import sys
1076
1077    arguments = argparse.ArgumentParser(
1078        description='Render mustache template.',
1079    )
1080    arguments.add_argument(
1081        'template',
1082        metavar='PATH',
1083        type=argparse.FileType('r'),
1084        help='template file',
1085    )
1086    arguments.add_argument(
1087        '-j', '--json',
1088        metavar='PATH',
1089        type=argparse.FileType('r'),
1090        default=sys.stdin,
1091        help='JSON file, default: stdin',
1092    )
1093    arguments.add_argument(
1094        '-o', '--output',
1095        metavar='PATH',
1096        type=argparse.FileType('w'),
1097        default=sys.stdout,
1098        help='output file, default: stdout',
1099    )
1100    args = arguments.parse_args(argv)
1101    try:
1102        args.output.write(render(args.template.read(), json.load(args.json)))
1103    finally:
1104        args.template.close()
1105        if args.json is not sys.stdin:
1106            args.json.close()
1107        if args.output is not sys.stdout:
1108            args.output.close()
1109
1110
1111if __name__ == '__main__':
1112    cli()
1113