Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
extract.py843 linesDownload Raw Back to messages
1"""2    babel.messages.extract3    ~~~~~~~~~~~~~~~~~~~~~~4 5    Basic infrastructure for extracting localizable messages from source files.6 7    This module defines an extensible system for collecting localizable message8    strings from a variety of sources. A native extractor for Python source9    files is builtin, extractors for other sources can be added using very10    simple plugins.11 12    The main entry points into the extraction functionality are the functions13    `extract_from_dir` and `extract_from_file`.14 15    :copyright: (c) 2013-2024 by the Babel Team.16    :license: BSD, see LICENSE for more details.17"""18from __future__ import annotations19 20import ast21import io22import os23import sys24import tokenize25from collections.abc import (26    Callable,27    Collection,28    Generator,29    Iterable,30    Mapping,31    MutableSequence,32)33from os.path import relpath34from textwrap import dedent35from tokenize import COMMENT, NAME, OP, STRING, generate_tokens36from typing import TYPE_CHECKING, Any37 38from babel.util import parse_encoding, parse_future_flags, pathmatch39 40if TYPE_CHECKING:41    from typing import IO, Final, Protocol42 43    from _typeshed import SupportsItems, SupportsRead, SupportsReadline44    from typing_extensions import TypeAlias, TypedDict45 46    class _PyOptions(TypedDict, total=False):47        encoding: str48 49    class _JSOptions(TypedDict, total=False):50        encoding: str51        jsx: bool52        template_string: bool53        parse_template_string: bool54 55    class _FileObj(SupportsRead[bytes], SupportsReadline[bytes], Protocol):56        def seek(self, __offset: int, __whence: int = ...) -> int: ...57        def tell(self) -> int: ...58 59    _SimpleKeyword: TypeAlias = tuple[int | tuple[int, int] | tuple[int, str], ...] | None60    _Keyword: TypeAlias = dict[int | None, _SimpleKeyword] | _SimpleKeyword61 62    # 5-tuple of (filename, lineno, messages, comments, context)63    _FileExtractionResult: TypeAlias = tuple[str, int, str | tuple[str, ...], list[str], str | None]64 65    # 4-tuple of (lineno, message, comments, context)66    _ExtractionResult: TypeAlias = tuple[int, str | tuple[str, ...], list[str], str | None]67 68    # Required arguments: fileobj, keywords, comment_tags, options69    # Return value: Iterable of (lineno, message, comments, context)70    _CallableExtractionMethod: TypeAlias = Callable[71        [_FileObj | IO[bytes], Mapping[str, _Keyword], Collection[str], Mapping[str, Any]],72        Iterable[_ExtractionResult],73    ]74 75    _ExtractionMethod: TypeAlias = _CallableExtractionMethod | str76 77GROUP_NAME: Final[str] = 'babel.extractors'78 79DEFAULT_KEYWORDS: dict[str, _Keyword] = {80    '_': None,81    'gettext': None,82    'ngettext': (1, 2),83    'ugettext': None,84    'ungettext': (1, 2),85    'dgettext': (2,),86    'dngettext': (2, 3),87    'N_': None,88    'pgettext': ((1, 'c'), 2),89    'npgettext': ((1, 'c'), 2, 3),90}91 92DEFAULT_MAPPING: list[tuple[str, str]] = [('**.py', 'python')]93 94# New tokens in Python 3.12, or None on older versions95FSTRING_START = getattr(tokenize, "FSTRING_START", None)96FSTRING_MIDDLE = getattr(tokenize, "FSTRING_MIDDLE", None)97FSTRING_END = getattr(tokenize, "FSTRING_END", None)98 99 100def _strip_comment_tags(comments: MutableSequence[str], tags: Iterable[str]):101    """Helper function for `extract` that strips comment tags from strings102    in a list of comment lines.  This functions operates in-place.103    """104    def _strip(line: str):105        for tag in tags:106            if line.startswith(tag):107                return line[len(tag):].strip()108        return line109    comments[:] = map(_strip, comments)110 111 112def default_directory_filter(dirpath: str | os.PathLike[str]) -> bool:113    subdir = os.path.basename(dirpath)114    # Legacy default behavior: ignore dot and underscore directories115    return not (subdir.startswith('.') or subdir.startswith('_'))116 117 118def extract_from_dir(119    dirname: str | os.PathLike[str] | None = None,120    method_map: Iterable[tuple[str, str]] = DEFAULT_MAPPING,121    options_map: SupportsItems[str, dict[str, Any]] | None = None,122    keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS,123    comment_tags: Collection[str] = (),124    callback: Callable[[str, str, dict[str, Any]], object] | None = None,125    strip_comment_tags: bool = False,126    directory_filter: Callable[[str], bool] | None = None,127) -> Generator[_FileExtractionResult, None, None]:128    """Extract messages from any source files found in the given directory.129 130    This function generates tuples of the form ``(filename, lineno, message,131    comments, context)``.132 133    Which extraction method is used per file is determined by the `method_map`134    parameter, which maps extended glob patterns to extraction method names.135    For example, the following is the default mapping:136 137    >>> method_map = [138    ...     ('**.py', 'python')139    ... ]140 141    This basically says that files with the filename extension ".py" at any142    level inside the directory should be processed by the "python" extraction143    method. Files that don't match any of the mapping patterns are ignored. See144    the documentation of the `pathmatch` function for details on the pattern145    syntax.146 147    The following extended mapping would also use the "genshi" extraction148    method on any file in "templates" subdirectory:149 150    >>> method_map = [151    ...     ('**/templates/**.*', 'genshi'),152    ...     ('**.py', 'python')153    ... ]154 155    The dictionary provided by the optional `options_map` parameter augments156    these mappings. It uses extended glob patterns as keys, and the values are157    dictionaries mapping options names to option values (both strings).158 159    The glob patterns of the `options_map` do not necessarily need to be the160    same as those used in the method mapping. For example, while all files in161    the ``templates`` folders in an application may be Genshi applications, the162    options for those files may differ based on extension:163 164    >>> options_map = {165    ...     '**/templates/**.txt': {166    ...         'template_class': 'genshi.template:TextTemplate',167    ...         'encoding': 'latin-1'168    ...     },169    ...     '**/templates/**.html': {170    ...         'include_attrs': ''171    ...     }172    ... }173 174    :param dirname: the path to the directory to extract messages from.  If175                    not given the current working directory is used.176    :param method_map: a list of ``(pattern, method)`` tuples that maps of177                       extraction method names to extended glob patterns178    :param options_map: a dictionary of additional options (optional)179    :param keywords: a dictionary mapping keywords (i.e. names of functions180                     that should be recognized as translation functions) to181                     tuples that specify which of their arguments contain182                     localizable strings183    :param comment_tags: a list of tags of translator comments to search for184                         and include in the results185    :param callback: a function that is called for every file that message are186                     extracted from, just before the extraction itself is187                     performed; the function is passed the filename, the name188                     of the extraction method and and the options dictionary as189                     positional arguments, in that order190    :param strip_comment_tags: a flag that if set to `True` causes all comment191                               tags to be removed from the collected comments.192    :param directory_filter: a callback to determine whether a directory should193                             be recursed into. Receives the full directory path;194                             should return True if the directory is valid.195    :see: `pathmatch`196    """197    if dirname is None:198        dirname = os.getcwd()199    if options_map is None:200        options_map = {}201    if directory_filter is None:202        directory_filter = default_directory_filter203 204    absname = os.path.abspath(dirname)205    for root, dirnames, filenames in os.walk(absname):206        dirnames[:] = [207            subdir for subdir in dirnames208            if directory_filter(os.path.join(root, subdir))209        ]210        dirnames.sort()211        filenames.sort()212        for filename in filenames:213            filepath = os.path.join(root, filename).replace(os.sep, '/')214 215            yield from check_and_call_extract_file(216                filepath,217                method_map,218                options_map,219                callback,220                keywords,221                comment_tags,222                strip_comment_tags,223                dirpath=absname,224            )225 226 227def check_and_call_extract_file(228    filepath: str | os.PathLike[str],229    method_map: Iterable[tuple[str, str]],230    options_map: SupportsItems[str, dict[str, Any]],231    callback: Callable[[str, str, dict[str, Any]], object] | None,232    keywords: Mapping[str, _Keyword],233    comment_tags: Collection[str],234    strip_comment_tags: bool,235    dirpath: str | os.PathLike[str] | None = None,236) -> Generator[_FileExtractionResult, None, None]:237    """Checks if the given file matches an extraction method mapping, and if so, calls extract_from_file.238 239    Note that the extraction method mappings are based relative to dirpath.240    So, given an absolute path to a file `filepath`, we want to check using241    just the relative path from `dirpath` to `filepath`.242 243    Yields 5-tuples (filename, lineno, messages, comments, context).244 245    :param filepath: An absolute path to a file that exists.246    :param method_map: a list of ``(pattern, method)`` tuples that maps of247                       extraction method names to extended glob patterns248    :param options_map: a dictionary of additional options (optional)249    :param callback: a function that is called for every file that message are250                     extracted from, just before the extraction itself is251                     performed; the function is passed the filename, the name252                     of the extraction method and and the options dictionary as253                     positional arguments, in that order254    :param keywords: a dictionary mapping keywords (i.e. names of functions255                     that should be recognized as translation functions) to256                     tuples that specify which of their arguments contain257                     localizable strings258    :param comment_tags: a list of tags of translator comments to search for259                         and include in the results260    :param strip_comment_tags: a flag that if set to `True` causes all comment261                               tags to be removed from the collected comments.262    :param dirpath: the path to the directory to extract messages from.263    :return: iterable of 5-tuples (filename, lineno, messages, comments, context)264    :rtype: Iterable[tuple[str, int, str|tuple[str], list[str], str|None]265    """266    # filename is the relative path from dirpath to the actual file267    filename = relpath(filepath, dirpath)268 269    for pattern, method in method_map:270        if not pathmatch(pattern, filename):271            continue272 273        options = {}274        for opattern, odict in options_map.items():275            if pathmatch(opattern, filename):276                options = odict277        if callback:278            callback(filename, method, options)279        for message_tuple in extract_from_file(280            method, filepath,281            keywords=keywords,282            comment_tags=comment_tags,283            options=options,284            strip_comment_tags=strip_comment_tags,285        ):286            yield (filename, *message_tuple)287 288        break289 290 291def extract_from_file(292    method: _ExtractionMethod,293    filename: str | os.PathLike[str],294    keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS,295    comment_tags: Collection[str] = (),296    options: Mapping[str, Any] | None = None,297    strip_comment_tags: bool = False,298) -> list[_ExtractionResult]:299    """Extract messages from a specific file.300 301    This function returns a list of tuples of the form ``(lineno, message, comments, context)``.302 303    :param filename: the path to the file to extract messages from304    :param method: a string specifying the extraction method (.e.g. "python")305    :param keywords: a dictionary mapping keywords (i.e. names of functions306                     that should be recognized as translation functions) to307                     tuples that specify which of their arguments contain308                     localizable strings309    :param comment_tags: a list of translator tags to search for and include310                         in the results311    :param strip_comment_tags: a flag that if set to `True` causes all comment312                               tags to be removed from the collected comments.313    :param options: a dictionary of additional options (optional)314    :returns: list of tuples of the form ``(lineno, message, comments, context)``315    :rtype: list[tuple[int, str|tuple[str], list[str], str|None]316    """317    if method == 'ignore':318        return []319 320    with open(filename, 'rb') as fileobj:321        return list(extract(method, fileobj, keywords, comment_tags,322                            options, strip_comment_tags))323 324 325def _match_messages_against_spec(lineno: int, messages: list[str|None], comments: list[str],326                                 fileobj: _FileObj, spec: tuple[int|tuple[int, str], ...]):327    translatable = []328    context = None329 330    # last_index is 1 based like the keyword spec331    last_index = len(messages)332    for index in spec:333        if isinstance(index, tuple): # (n, 'c')334            context = messages[index[0] - 1]335            continue336        if last_index < index:337            # Not enough arguments338            return339        message = messages[index - 1]340        if message is None:341            return342        translatable.append(message)343 344    # keyword spec indexes are 1 based, therefore '-1'345    if isinstance(spec[0], tuple):346        # context-aware *gettext method347        first_msg_index = spec[1] - 1348    else:349        first_msg_index = spec[0] - 1350    # An empty string msgid isn't valid, emit a warning351    if not messages[first_msg_index]:352        filename = (getattr(fileobj, "name", None) or "(unknown)")353        sys.stderr.write(354            f"{filename}:{lineno}: warning: Empty msgid.  It is reserved by GNU gettext: gettext(\"\") "355            f"returns the header entry with meta information, not the empty string.\n",356        )357        return358 359    translatable = tuple(translatable)360    if len(translatable) == 1:361        translatable = translatable[0]362 363    return lineno, translatable, comments, context364 365 366def extract(367    method: _ExtractionMethod,368    fileobj: _FileObj,369    keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS,370    comment_tags: Collection[str] = (),371    options: Mapping[str, Any] | None = None,372    strip_comment_tags: bool = False,373) -> Generator[_ExtractionResult, None, None]:374    """Extract messages from the given file-like object using the specified375    extraction method.376 377    This function returns tuples of the form ``(lineno, message, comments, context)``.378 379    The implementation dispatches the actual extraction to plugins, based on the380    value of the ``method`` parameter.381 382    >>> source = b'''# foo module383    ... def run(argv):384    ...    print(_('Hello, world!'))385    ... '''386 387    >>> from io import BytesIO388    >>> for message in extract('python', BytesIO(source)):389    ...     print(message)390    (3, u'Hello, world!', [], None)391 392    :param method: an extraction method (a callable), or393                   a string specifying the extraction method (.e.g. "python");394                   if this is a simple name, the extraction function will be395                   looked up by entry point; if it is an explicit reference396                   to a function (of the form ``package.module:funcname`` or397                   ``package.module.funcname``), the corresponding function398                   will be imported and used399    :param fileobj: the file-like object the messages should be extracted from400    :param keywords: a dictionary mapping keywords (i.e. names of functions401                     that should be recognized as translation functions) to402                     tuples that specify which of their arguments contain403                     localizable strings404    :param comment_tags: a list of translator tags to search for and include405                         in the results406    :param options: a dictionary of additional options (optional)407    :param strip_comment_tags: a flag that if set to `True` causes all comment408                               tags to be removed from the collected comments.409    :raise ValueError: if the extraction method is not registered410    :returns: iterable of tuples of the form ``(lineno, message, comments, context)``411    :rtype: Iterable[tuple[int, str|tuple[str], list[str], str|None]412    """413    func = None414    if callable(method):415        func = method416    elif ':' in method or '.' in method:417        if ':' not in method:418            lastdot = method.rfind('.')419            module, attrname = method[:lastdot], method[lastdot + 1:]420        else:421            module, attrname = method.split(':', 1)422        func = getattr(__import__(module, {}, {}, [attrname]), attrname)423    else:424        try:425            from pkg_resources import working_set426        except ImportError:427            pass428        else:429            for entry_point in working_set.iter_entry_points(GROUP_NAME,430                                                             method):431                func = entry_point.load(require=True)432                break433        if func is None:434            # if pkg_resources is not available or no usable egg-info was found435            # (see #230), we resort to looking up the builtin extractors436            # directly437            builtin = {438                'ignore': extract_nothing,439                'python': extract_python,440                'javascript': extract_javascript,441            }442            func = builtin.get(method)443 444    if func is None:445        raise ValueError(f"Unknown extraction method {method!r}")446 447    results = func(fileobj, keywords.keys(), comment_tags,448                   options=options or {})449 450    for lineno, funcname, messages, comments in results:451        if not isinstance(messages, (list, tuple)):452            messages = [messages]453        if not messages:454            continue455 456        specs = keywords[funcname] or None if funcname else None457        # {None: x} may be collapsed into x for backwards compatibility.458        if not isinstance(specs, dict):459            specs = {None: specs}460 461        if strip_comment_tags:462            _strip_comment_tags(comments, comment_tags)463 464        # None matches all arities.465        for arity in (None, len(messages)):466            try:467                spec = specs[arity]468            except KeyError:469                continue470            if spec is None:471                spec = (1,)472            result = _match_messages_against_spec(lineno, messages, comments, fileobj, spec)473            if result is not None:474                yield result475 476 477def extract_nothing(478    fileobj: _FileObj,479    keywords: Mapping[str, _Keyword],480    comment_tags: Collection[str],481    options: Mapping[str, Any],482) -> list[_ExtractionResult]:483    """Pseudo extractor that does not actually extract anything, but simply484    returns an empty list.485    """486    return []487 488 489def extract_python(490    fileobj: IO[bytes],491    keywords: Mapping[str, _Keyword],492    comment_tags: Collection[str],493    options: _PyOptions,494) -> Generator[_ExtractionResult, None, None]:495    """Extract messages from Python source code.496 497    It returns an iterator yielding tuples in the following form ``(lineno,498    funcname, message, comments)``.499 500    :param fileobj: the seekable, file-like object the messages should be501                    extracted from502    :param keywords: a list of keywords (i.e. function names) that should be503                     recognized as translation functions504    :param comment_tags: a list of translator tags to search for and include505                         in the results506    :param options: a dictionary of additional options (optional)507    :rtype: ``iterator``508    """509    funcname = lineno = message_lineno = None510    call_stack = -1511    buf = []512    messages = []513    translator_comments = []514    in_def = in_translator_comments = False515    comment_tag = None516 517    encoding = parse_encoding(fileobj) or options.get('encoding', 'UTF-8')518    future_flags = parse_future_flags(fileobj, encoding)519    next_line = lambda: fileobj.readline().decode(encoding)520 521    tokens = generate_tokens(next_line)522 523    # Current prefix of a Python 3.12 (PEP 701) f-string, or None if we're not524    # currently parsing one.525    current_fstring_start = None526 527    for tok, value, (lineno, _), _, _ in tokens:528        if call_stack == -1 and tok == NAME and value in ('def', 'class'):529            in_def = True530        elif tok == OP and value == '(':531            if in_def:532                # Avoid false positives for declarations such as:533                # def gettext(arg='message'):534                in_def = False535                continue536            if funcname:537                message_lineno = lineno538                call_stack += 1539        elif in_def and tok == OP and value == ':':540            # End of a class definition without parens541            in_def = False542            continue543        elif call_stack == -1 and tok == COMMENT:544            # Strip the comment token from the line545            value = value[1:].strip()546            if in_translator_comments and \547                    translator_comments[-1][0] == lineno - 1:548                # We're already inside a translator comment, continue appending549                translator_comments.append((lineno, value))550                continue551            # If execution reaches this point, let's see if comment line552            # starts with one of the comment tags553            for comment_tag in comment_tags:554                if value.startswith(comment_tag):555                    in_translator_comments = True556                    translator_comments.append((lineno, value))557                    break558        elif funcname and call_stack == 0:559            nested = (tok == NAME and value in keywords)560            if (tok == OP and value == ')') or nested:561                if buf:562                    messages.append(''.join(buf))563                    del buf[:]564                else:565                    messages.append(None)566 567                messages = tuple(messages) if len(messages) > 1 else messages[0]568                # Comments don't apply unless they immediately569                # precede the message570                if translator_comments and \571                        translator_comments[-1][0] < message_lineno - 1:572                    translator_comments = []573 574                yield (message_lineno, funcname, messages,575                       [comment[1] for comment in translator_comments])576 577                funcname = lineno = message_lineno = None578                call_stack = -1579                messages = []580                translator_comments = []581                in_translator_comments = False582                if nested:583                    funcname = value584            elif tok == STRING:585                val = _parse_python_string(value, encoding, future_flags)586                if val is not None:587                    buf.append(val)588 589            # Python 3.12+, see https://peps.python.org/pep-0701/#new-tokens590            elif tok == FSTRING_START:591                current_fstring_start = value592            elif tok == FSTRING_MIDDLE:593                if current_fstring_start is not None:594                    current_fstring_start += value595            elif tok == FSTRING_END:596                if current_fstring_start is not None:597                    fstring = current_fstring_start + value598                    val = _parse_python_string(fstring, encoding, future_flags)599                    if val is not None:600                        buf.append(val)601 602            elif tok == OP and value == ',':603                if buf:604                    messages.append(''.join(buf))605                    del buf[:]606                else:607                    messages.append(None)608                if translator_comments:609                    # We have translator comments, and since we're on a610                    # comma(,) user is allowed to break into a new line611                    # Let's increase the last comment's lineno in order612                    # for the comment to still be a valid one613                    old_lineno, old_comment = translator_comments.pop()614                    translator_comments.append((old_lineno + 1, old_comment))615        elif call_stack > 0 and tok == OP and value == ')':616            call_stack -= 1617        elif funcname and call_stack == -1:618            funcname = None619        elif tok == NAME and value in keywords:620            funcname = value621 622        if (current_fstring_start is not None623            and tok not in {FSTRING_START, FSTRING_MIDDLE}624        ):625            # In Python 3.12, tokens other than FSTRING_* mean the626            # f-string is dynamic, so we don't wan't to extract it.627            # And if it's FSTRING_END, we've already handled it above.628            # Let's forget that we're in an f-string.629            current_fstring_start = None630 631 632def _parse_python_string(value: str, encoding: str, future_flags: int) -> str | None:633    # Unwrap quotes in a safe manner, maintaining the string's encoding634    # https://sourceforge.net/tracker/?func=detail&atid=355470&aid=617979&group_id=5470635    code = compile(636        f'# coding={str(encoding)}\n{value}',637        '<string>',638        'eval',639        ast.PyCF_ONLY_AST | future_flags,640    )641    if isinstance(code, ast.Expression):642        body = code.body643        if isinstance(body, ast.Str):644            return body.s645        if isinstance(body, ast.JoinedStr):  # f-string646            if all(isinstance(node, ast.Str) for node in body.values):647                return ''.join(node.s for node in body.values)648            if all(isinstance(node, ast.Constant) for node in body.values):649                return ''.join(str(node.value) for node in body.values)650            # TODO: we could raise an error or warning when not all nodes are constants651    return None652 653 654def extract_javascript(655    fileobj: _FileObj,656    keywords: Mapping[str, _Keyword],657    comment_tags: Collection[str],658    options: _JSOptions,659    lineno: int = 1,660) -> Generator[_ExtractionResult, None, None]:661    """Extract messages from JavaScript source code.662 663    :param fileobj: the seekable, file-like object the messages should be664                    extracted from665    :param keywords: a list of keywords (i.e. function names) that should be666                     recognized as translation functions667    :param comment_tags: a list of translator tags to search for and include668                         in the results669    :param options: a dictionary of additional options (optional)670                    Supported options are:671                    * `jsx` -- set to false to disable JSX/E4X support.672                    * `template_string` -- if `True`, supports gettext(`key`)673                    * `parse_template_string` -- if `True` will parse the674                                                 contents of javascript675                                                 template strings.676    :param lineno: line number offset (for parsing embedded fragments)677    """678    from babel.messages.jslexer import Token, tokenize, unquote_string679    funcname = message_lineno = None680    messages = []681    last_argument = None682    translator_comments = []683    concatenate_next = False684    encoding = options.get('encoding', 'utf-8')685    last_token = None686    call_stack = -1687    dotted = any('.' in kw for kw in keywords)688    for token in tokenize(689        fileobj.read().decode(encoding),690        jsx=options.get("jsx", True),691        template_string=options.get("template_string", True),692        dotted=dotted,693        lineno=lineno,694    ):695        if (  # Turn keyword`foo` expressions into keyword("foo") calls:696            funcname and  # have a keyword...697            (last_token and last_token.type == 'name') and  # we've seen nothing after the keyword...698            token.type == 'template_string'  # this is a template string699        ):700            message_lineno = token.lineno701            messages = [unquote_string(token.value)]702            call_stack = 0703            token = Token('operator', ')', token.lineno)704 705        if options.get('parse_template_string') and not funcname and token.type == 'template_string':706            yield from parse_template_string(token.value, keywords, comment_tags, options, token.lineno)707 708        elif token.type == 'operator' and token.value == '(':709            if funcname:710                message_lineno = token.lineno711                call_stack += 1712 713        elif call_stack == -1 and token.type == 'linecomment':714            value = token.value[2:].strip()715            if translator_comments and \716               translator_comments[-1][0] == token.lineno - 1:717                translator_comments.append((token.lineno, value))718                continue719 720            for comment_tag in comment_tags:721                if value.startswith(comment_tag):722                    translator_comments.append((token.lineno, value.strip()))723                    break724 725        elif token.type == 'multilinecomment':726            # only one multi-line comment may precede a translation727            translator_comments = []728            value = token.value[2:-2].strip()729            for comment_tag in comment_tags:730                if value.startswith(comment_tag):731                    lines = value.splitlines()732                    if lines:733                        lines[0] = lines[0].strip()734                        lines[1:] = dedent('\n'.join(lines[1:])).splitlines()735                        for offset, line in enumerate(lines):736                            translator_comments.append((token.lineno + offset,737                                                        line))738                    break739 740        elif funcname and call_stack == 0:741            if token.type == 'operator' and token.value == ')':742                if last_argument is not None:743                    messages.append(last_argument)744                if len(messages) > 1:745                    messages = tuple(messages)746                elif messages:747                    messages = messages[0]748                else:749                    messages = None750 751                # Comments don't apply unless they immediately precede the752                # message753                if translator_comments and \754                   translator_comments[-1][0] < message_lineno - 1:755                    translator_comments = []756 757                if messages is not None:758                    yield (message_lineno, funcname, messages,759                           [comment[1] for comment in translator_comments])760 761                funcname = message_lineno = last_argument = None762                concatenate_next = False763                translator_comments = []764                messages = []765                call_stack = -1766 767            elif token.type in ('string', 'template_string'):768                new_value = unquote_string(token.value)769                if concatenate_next:770                    last_argument = (last_argument or '') + new_value771                    concatenate_next = False772                else:773                    last_argument = new_value774 775            elif token.type == 'operator':776                if token.value == ',':777                    if last_argument is not None:778                        messages.append(last_argument)779                        last_argument = None780                    else:781                        messages.append(None)782                    concatenate_next = False783                elif token.value == '+':784                    concatenate_next = True785 786        elif call_stack > 0 and token.type == 'operator' \787                and token.value == ')':788            call_stack -= 1789 790        elif funcname and call_stack == -1:791            funcname = None792 793        elif call_stack == -1 and token.type == 'name' and \794            token.value in keywords and \795            (last_token is None or last_token.type != 'name' or796             last_token.value != 'function'):797            funcname = token.value798 799        last_token = token800 801 802def parse_template_string(803    template_string: str,804    keywords: Mapping[str, _Keyword],805    comment_tags: Collection[str],806    options: _JSOptions,807    lineno: int = 1,808) -> Generator[_ExtractionResult, None, None]:809    """Parse JavaScript template string.810 811    :param template_string: the template string to be parsed812    :param keywords: a list of keywords (i.e. function names) that should be813                     recognized as translation functions814    :param comment_tags: a list of translator tags to search for and include815                         in the results816    :param options: a dictionary of additional options (optional)817    :param lineno: starting line number (optional)818    """819    from babel.messages.jslexer import line_re820    prev_character = None821    level = 0822    inside_str = False823    expression_contents = ''824    for character in template_string[1:-1]:825        if not inside_str and character in ('"', "'", '`'):826            inside_str = character827        elif inside_str == character and prev_character != r'\\':828            inside_str = False829        if level:830            expression_contents += character831        if not inside_str:832            if character == '{' and prev_character == '$':833                level += 1834            elif level and character == '}':835                level -= 1836                if level == 0 and expression_contents:837                    expression_contents = expression_contents[0:-1]838                    fake_file_obj = io.BytesIO(expression_contents.encode())839                    yield from extract_javascript(fake_file_obj, keywords, comment_tags, options, lineno)840                    lineno += len(line_re.findall(expression_contents))841                    expression_contents = ''842        prev_character = character843 
codekingpro/portable-devtools · Team Ai