You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

880 lines
31KB

  1. """Extension API for adding custom tags and behavior."""
  2. import pprint
  3. import re
  4. import typing as t
  5. import warnings
  6. from markupsafe import Markup
  7. from . import defaults
  8. from . import nodes
  9. from .environment import Environment
  10. from .exceptions import TemplateAssertionError
  11. from .exceptions import TemplateSyntaxError
  12. from .runtime import concat # type: ignore
  13. from .runtime import Context
  14. from .runtime import Undefined
  15. from .utils import import_string
  16. from .utils import pass_context
  17. if t.TYPE_CHECKING:
  18. import typing_extensions as te
  19. from .lexer import Token
  20. from .lexer import TokenStream
  21. from .parser import Parser
  22. class _TranslationsBasic(te.Protocol):
  23. def gettext(self, message: str) -> str:
  24. ...
  25. def ngettext(self, singular: str, plural: str, n: int) -> str:
  26. pass
  27. class _TranslationsContext(_TranslationsBasic):
  28. def pgettext(self, context: str, message: str) -> str:
  29. ...
  30. def npgettext(self, context: str, singular: str, plural: str, n: int) -> str:
  31. ...
  32. _SupportedTranslations = t.Union[_TranslationsBasic, _TranslationsContext]
  33. # I18N functions available in Jinja templates. If the I18N library
  34. # provides ugettext, it will be assigned to gettext.
  35. GETTEXT_FUNCTIONS: t.Tuple[str, ...] = (
  36. "_",
  37. "gettext",
  38. "ngettext",
  39. "pgettext",
  40. "npgettext",
  41. )
  42. _ws_re = re.compile(r"\s*\n\s*")
  43. class Extension:
  44. """Extensions can be used to add extra functionality to the Jinja template
  45. system at the parser level. Custom extensions are bound to an environment
  46. but may not store environment specific data on `self`. The reason for
  47. this is that an extension can be bound to another environment (for
  48. overlays) by creating a copy and reassigning the `environment` attribute.
  49. As extensions are created by the environment they cannot accept any
  50. arguments for configuration. One may want to work around that by using
  51. a factory function, but that is not possible as extensions are identified
  52. by their import name. The correct way to configure the extension is
  53. storing the configuration values on the environment. Because this way the
  54. environment ends up acting as central configuration storage the
  55. attributes may clash which is why extensions have to ensure that the names
  56. they choose for configuration are not too generic. ``prefix`` for example
  57. is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
  58. name as includes the name of the extension (fragment cache).
  59. """
  60. identifier: t.ClassVar[str]
  61. def __init_subclass__(cls) -> None:
  62. cls.identifier = f"{cls.__module__}.{cls.__name__}"
  63. #: if this extension parses this is the list of tags it's listening to.
  64. tags: t.Set[str] = set()
  65. #: the priority of that extension. This is especially useful for
  66. #: extensions that preprocess values. A lower value means higher
  67. #: priority.
  68. #:
  69. #: .. versionadded:: 2.4
  70. priority = 100
  71. def __init__(self, environment: Environment) -> None:
  72. self.environment = environment
  73. def bind(self, environment: Environment) -> "Extension":
  74. """Create a copy of this extension bound to another environment."""
  75. rv = t.cast(Extension, object.__new__(self.__class__))
  76. rv.__dict__.update(self.__dict__)
  77. rv.environment = environment
  78. return rv
  79. def preprocess(
  80. self, source: str, name: t.Optional[str], filename: t.Optional[str] = None
  81. ) -> str:
  82. """This method is called before the actual lexing and can be used to
  83. preprocess the source. The `filename` is optional. The return value
  84. must be the preprocessed source.
  85. """
  86. return source
  87. def filter_stream(
  88. self, stream: "TokenStream"
  89. ) -> t.Union["TokenStream", t.Iterable["Token"]]:
  90. """It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
  91. to filter tokens returned. This method has to return an iterable of
  92. :class:`~jinja2.lexer.Token`\\s, but it doesn't have to return a
  93. :class:`~jinja2.lexer.TokenStream`.
  94. """
  95. return stream
  96. def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
  97. """If any of the :attr:`tags` matched this method is called with the
  98. parser as first argument. The token the parser stream is pointing at
  99. is the name token that matched. This method has to return one or a
  100. list of multiple nodes.
  101. """
  102. raise NotImplementedError()
  103. def attr(
  104. self, name: str, lineno: t.Optional[int] = None
  105. ) -> nodes.ExtensionAttribute:
  106. """Return an attribute node for the current extension. This is useful
  107. to pass constants on extensions to generated template code.
  108. ::
  109. self.attr('_my_attribute', lineno=lineno)
  110. """
  111. return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
  112. def call_method(
  113. self,
  114. name: str,
  115. args: t.Optional[t.List[nodes.Expr]] = None,
  116. kwargs: t.Optional[t.List[nodes.Keyword]] = None,
  117. dyn_args: t.Optional[nodes.Expr] = None,
  118. dyn_kwargs: t.Optional[nodes.Expr] = None,
  119. lineno: t.Optional[int] = None,
  120. ) -> nodes.Call:
  121. """Call a method of the extension. This is a shortcut for
  122. :meth:`attr` + :class:`jinja2.nodes.Call`.
  123. """
  124. if args is None:
  125. args = []
  126. if kwargs is None:
  127. kwargs = []
  128. return nodes.Call(
  129. self.attr(name, lineno=lineno),
  130. args,
  131. kwargs,
  132. dyn_args,
  133. dyn_kwargs,
  134. lineno=lineno,
  135. )
  136. @pass_context
  137. def _gettext_alias(
  138. __context: Context, *args: t.Any, **kwargs: t.Any
  139. ) -> t.Union[t.Any, Undefined]:
  140. return __context.call(__context.resolve("gettext"), *args, **kwargs)
  141. def _make_new_gettext(func: t.Callable[[str], str]) -> t.Callable[..., str]:
  142. @pass_context
  143. def gettext(__context: Context, __string: str, **variables: t.Any) -> str:
  144. rv = __context.call(func, __string)
  145. if __context.eval_ctx.autoescape:
  146. rv = Markup(rv)
  147. # Always treat as a format string, even if there are no
  148. # variables. This makes translation strings more consistent
  149. # and predictable. This requires escaping
  150. return rv % variables # type: ignore
  151. return gettext
  152. def _make_new_ngettext(func: t.Callable[[str, str, int], str]) -> t.Callable[..., str]:
  153. @pass_context
  154. def ngettext(
  155. __context: Context,
  156. __singular: str,
  157. __plural: str,
  158. __num: int,
  159. **variables: t.Any,
  160. ) -> str:
  161. variables.setdefault("num", __num)
  162. rv = __context.call(func, __singular, __plural, __num)
  163. if __context.eval_ctx.autoescape:
  164. rv = Markup(rv)
  165. # Always treat as a format string, see gettext comment above.
  166. return rv % variables # type: ignore
  167. return ngettext
  168. def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str]:
  169. @pass_context
  170. def pgettext(
  171. __context: Context, __string_ctx: str, __string: str, **variables: t.Any
  172. ) -> str:
  173. variables.setdefault("context", __string_ctx)
  174. rv = __context.call(func, __string_ctx, __string)
  175. if __context.eval_ctx.autoescape:
  176. rv = Markup(rv)
  177. # Always treat as a format string, see gettext comment above.
  178. return rv % variables # type: ignore
  179. return pgettext
  180. def _make_new_npgettext(
  181. func: t.Callable[[str, str, str, int], str]
  182. ) -> t.Callable[..., str]:
  183. @pass_context
  184. def npgettext(
  185. __context: Context,
  186. __string_ctx: str,
  187. __singular: str,
  188. __plural: str,
  189. __num: int,
  190. **variables: t.Any,
  191. ) -> str:
  192. variables.setdefault("context", __string_ctx)
  193. variables.setdefault("num", __num)
  194. rv = __context.call(func, __string_ctx, __singular, __plural, __num)
  195. if __context.eval_ctx.autoescape:
  196. rv = Markup(rv)
  197. # Always treat as a format string, see gettext comment above.
  198. return rv % variables # type: ignore
  199. return npgettext
  200. class InternationalizationExtension(Extension):
  201. """This extension adds gettext support to Jinja."""
  202. tags = {"trans"}
  203. # TODO: the i18n extension is currently reevaluating values in a few
  204. # situations. Take this example:
  205. # {% trans count=something() %}{{ count }} foo{% pluralize
  206. # %}{{ count }} fooss{% endtrans %}
  207. # something is called twice here. One time for the gettext value and
  208. # the other time for the n-parameter of the ngettext function.
  209. def __init__(self, environment: Environment) -> None:
  210. super().__init__(environment)
  211. environment.globals["_"] = _gettext_alias
  212. environment.extend(
  213. install_gettext_translations=self._install,
  214. install_null_translations=self._install_null,
  215. install_gettext_callables=self._install_callables,
  216. uninstall_gettext_translations=self._uninstall,
  217. extract_translations=self._extract,
  218. newstyle_gettext=False,
  219. )
  220. def _install(
  221. self, translations: "_SupportedTranslations", newstyle: t.Optional[bool] = None
  222. ) -> None:
  223. # ugettext and ungettext are preferred in case the I18N library
  224. # is providing compatibility with older Python versions.
  225. gettext = getattr(translations, "ugettext", None)
  226. if gettext is None:
  227. gettext = translations.gettext
  228. ngettext = getattr(translations, "ungettext", None)
  229. if ngettext is None:
  230. ngettext = translations.ngettext
  231. pgettext = getattr(translations, "pgettext", None)
  232. npgettext = getattr(translations, "npgettext", None)
  233. self._install_callables(
  234. gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
  235. )
  236. def _install_null(self, newstyle: t.Optional[bool] = None) -> None:
  237. import gettext
  238. translations = gettext.NullTranslations()
  239. if hasattr(translations, "pgettext"):
  240. # Python < 3.8
  241. pgettext = translations.pgettext # type: ignore
  242. else:
  243. def pgettext(c: str, s: str) -> str:
  244. return s
  245. if hasattr(translations, "npgettext"):
  246. npgettext = translations.npgettext # type: ignore
  247. else:
  248. def npgettext(c: str, s: str, p: str, n: int) -> str:
  249. return s if n == 1 else p
  250. self._install_callables(
  251. gettext=translations.gettext,
  252. ngettext=translations.ngettext,
  253. newstyle=newstyle,
  254. pgettext=pgettext,
  255. npgettext=npgettext,
  256. )
  257. def _install_callables(
  258. self,
  259. gettext: t.Callable[[str], str],
  260. ngettext: t.Callable[[str, str, int], str],
  261. newstyle: t.Optional[bool] = None,
  262. pgettext: t.Optional[t.Callable[[str, str], str]] = None,
  263. npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
  264. ) -> None:
  265. if newstyle is not None:
  266. self.environment.newstyle_gettext = newstyle # type: ignore
  267. if self.environment.newstyle_gettext: # type: ignore
  268. gettext = _make_new_gettext(gettext)
  269. ngettext = _make_new_ngettext(ngettext)
  270. if pgettext is not None:
  271. pgettext = _make_new_pgettext(pgettext)
  272. if npgettext is not None:
  273. npgettext = _make_new_npgettext(npgettext)
  274. self.environment.globals.update(
  275. gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
  276. )
  277. def _uninstall(self, translations: "_SupportedTranslations") -> None:
  278. for key in ("gettext", "ngettext", "pgettext", "npgettext"):
  279. self.environment.globals.pop(key, None)
  280. def _extract(
  281. self,
  282. source: t.Union[str, nodes.Template],
  283. gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
  284. ) -> t.Iterator[
  285. t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
  286. ]:
  287. if isinstance(source, str):
  288. source = self.environment.parse(source)
  289. return extract_from_ast(source, gettext_functions)
  290. def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
  291. """Parse a translatable tag."""
  292. lineno = next(parser.stream).lineno
  293. num_called_num = False
  294. # find all the variables referenced. Additionally a variable can be
  295. # defined in the body of the trans block too, but this is checked at
  296. # a later state.
  297. plural_expr: t.Optional[nodes.Expr] = None
  298. plural_expr_assignment: t.Optional[nodes.Assign] = None
  299. variables: t.Dict[str, nodes.Expr] = {}
  300. trimmed = None
  301. while parser.stream.current.type != "block_end":
  302. if variables:
  303. parser.stream.expect("comma")
  304. # skip colon for python compatibility
  305. if parser.stream.skip_if("colon"):
  306. break
  307. token = parser.stream.expect("name")
  308. if token.value in variables:
  309. parser.fail(
  310. f"translatable variable {token.value!r} defined twice.",
  311. token.lineno,
  312. exc=TemplateAssertionError,
  313. )
  314. # expressions
  315. if parser.stream.current.type == "assign":
  316. next(parser.stream)
  317. variables[token.value] = var = parser.parse_expression()
  318. elif trimmed is None and token.value in ("trimmed", "notrimmed"):
  319. trimmed = token.value == "trimmed"
  320. continue
  321. else:
  322. variables[token.value] = var = nodes.Name(token.value, "load")
  323. if plural_expr is None:
  324. if isinstance(var, nodes.Call):
  325. plural_expr = nodes.Name("_trans", "load")
  326. variables[token.value] = plural_expr
  327. plural_expr_assignment = nodes.Assign(
  328. nodes.Name("_trans", "store"), var
  329. )
  330. else:
  331. plural_expr = var
  332. num_called_num = token.value == "num"
  333. parser.stream.expect("block_end")
  334. plural = None
  335. have_plural = False
  336. referenced = set()
  337. # now parse until endtrans or pluralize
  338. singular_names, singular = self._parse_block(parser, True)
  339. if singular_names:
  340. referenced.update(singular_names)
  341. if plural_expr is None:
  342. plural_expr = nodes.Name(singular_names[0], "load")
  343. num_called_num = singular_names[0] == "num"
  344. # if we have a pluralize block, we parse that too
  345. if parser.stream.current.test("name:pluralize"):
  346. have_plural = True
  347. next(parser.stream)
  348. if parser.stream.current.type != "block_end":
  349. token = parser.stream.expect("name")
  350. if token.value not in variables:
  351. parser.fail(
  352. f"unknown variable {token.value!r} for pluralization",
  353. token.lineno,
  354. exc=TemplateAssertionError,
  355. )
  356. plural_expr = variables[token.value]
  357. num_called_num = token.value == "num"
  358. parser.stream.expect("block_end")
  359. plural_names, plural = self._parse_block(parser, False)
  360. next(parser.stream)
  361. referenced.update(plural_names)
  362. else:
  363. next(parser.stream)
  364. # register free names as simple name expressions
  365. for name in referenced:
  366. if name not in variables:
  367. variables[name] = nodes.Name(name, "load")
  368. if not have_plural:
  369. plural_expr = None
  370. elif plural_expr is None:
  371. parser.fail("pluralize without variables", lineno)
  372. if trimmed is None:
  373. trimmed = self.environment.policies["ext.i18n.trimmed"]
  374. if trimmed:
  375. singular = self._trim_whitespace(singular)
  376. if plural:
  377. plural = self._trim_whitespace(plural)
  378. node = self._make_node(
  379. singular,
  380. plural,
  381. variables,
  382. plural_expr,
  383. bool(referenced),
  384. num_called_num and have_plural,
  385. )
  386. node.set_lineno(lineno)
  387. if plural_expr_assignment is not None:
  388. return [plural_expr_assignment, node]
  389. else:
  390. return node
  391. def _trim_whitespace(self, string: str, _ws_re: t.Pattern[str] = _ws_re) -> str:
  392. return _ws_re.sub(" ", string.strip())
  393. def _parse_block(
  394. self, parser: "Parser", allow_pluralize: bool
  395. ) -> t.Tuple[t.List[str], str]:
  396. """Parse until the next block tag with a given name."""
  397. referenced = []
  398. buf = []
  399. while True:
  400. if parser.stream.current.type == "data":
  401. buf.append(parser.stream.current.value.replace("%", "%%"))
  402. next(parser.stream)
  403. elif parser.stream.current.type == "variable_begin":
  404. next(parser.stream)
  405. name = parser.stream.expect("name").value
  406. referenced.append(name)
  407. buf.append(f"%({name})s")
  408. parser.stream.expect("variable_end")
  409. elif parser.stream.current.type == "block_begin":
  410. next(parser.stream)
  411. if parser.stream.current.test("name:endtrans"):
  412. break
  413. elif parser.stream.current.test("name:pluralize"):
  414. if allow_pluralize:
  415. break
  416. parser.fail(
  417. "a translatable section can have only one pluralize section"
  418. )
  419. parser.fail(
  420. "control structures in translatable sections are not allowed"
  421. )
  422. elif parser.stream.eos:
  423. parser.fail("unclosed translation block")
  424. else:
  425. raise RuntimeError("internal parser error")
  426. return referenced, concat(buf)
  427. def _make_node(
  428. self,
  429. singular: str,
  430. plural: t.Optional[str],
  431. variables: t.Dict[str, nodes.Expr],
  432. plural_expr: t.Optional[nodes.Expr],
  433. vars_referenced: bool,
  434. num_called_num: bool,
  435. ) -> nodes.Output:
  436. """Generates a useful node from the data provided."""
  437. newstyle = self.environment.newstyle_gettext # type: ignore
  438. node: nodes.Expr
  439. # no variables referenced? no need to escape for old style
  440. # gettext invocations only if there are vars.
  441. if not vars_referenced and not newstyle:
  442. singular = singular.replace("%%", "%")
  443. if plural:
  444. plural = plural.replace("%%", "%")
  445. # singular only:
  446. if plural_expr is None:
  447. gettext = nodes.Name("gettext", "load")
  448. node = nodes.Call(gettext, [nodes.Const(singular)], [], None, None)
  449. # singular and plural
  450. else:
  451. ngettext = nodes.Name("ngettext", "load")
  452. node = nodes.Call(
  453. ngettext,
  454. [nodes.Const(singular), nodes.Const(plural), plural_expr],
  455. [],
  456. None,
  457. None,
  458. )
  459. # in case newstyle gettext is used, the method is powerful
  460. # enough to handle the variable expansion and autoescape
  461. # handling itself
  462. if newstyle:
  463. for key, value in variables.items():
  464. # the function adds that later anyways in case num was
  465. # called num, so just skip it.
  466. if num_called_num and key == "num":
  467. continue
  468. node.kwargs.append(nodes.Keyword(key, value))
  469. # otherwise do that here
  470. else:
  471. # mark the return value as safe if we are in an
  472. # environment with autoescaping turned on
  473. node = nodes.MarkSafeIfAutoescape(node)
  474. if variables:
  475. node = nodes.Mod(
  476. node,
  477. nodes.Dict(
  478. [
  479. nodes.Pair(nodes.Const(key), value)
  480. for key, value in variables.items()
  481. ]
  482. ),
  483. )
  484. return nodes.Output([node])
  485. class ExprStmtExtension(Extension):
  486. """Adds a `do` tag to Jinja that works like the print statement just
  487. that it doesn't print the return value.
  488. """
  489. tags = {"do"}
  490. def parse(self, parser: "Parser") -> nodes.ExprStmt:
  491. node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
  492. node.node = parser.parse_tuple()
  493. return node
  494. class LoopControlExtension(Extension):
  495. """Adds break and continue to the template engine."""
  496. tags = {"break", "continue"}
  497. def parse(self, parser: "Parser") -> t.Union[nodes.Break, nodes.Continue]:
  498. token = next(parser.stream)
  499. if token.value == "break":
  500. return nodes.Break(lineno=token.lineno)
  501. return nodes.Continue(lineno=token.lineno)
  502. class WithExtension(Extension):
  503. def __init__(self, environment: Environment) -> None:
  504. super().__init__(environment)
  505. warnings.warn(
  506. "The 'with' extension is deprecated and will be removed in"
  507. " Jinja 3.1. This is built in now.",
  508. DeprecationWarning,
  509. stacklevel=3,
  510. )
  511. class AutoEscapeExtension(Extension):
  512. def __init__(self, environment: Environment) -> None:
  513. super().__init__(environment)
  514. warnings.warn(
  515. "The 'autoescape' extension is deprecated and will be"
  516. " removed in Jinja 3.1. This is built in now.",
  517. DeprecationWarning,
  518. stacklevel=3,
  519. )
  520. class DebugExtension(Extension):
  521. """A ``{% debug %}`` tag that dumps the available variables,
  522. filters, and tests.
  523. .. code-block:: html+jinja
  524. <pre>{% debug %}</pre>
  525. .. code-block:: text
  526. {'context': {'cycler': <class 'jinja2.utils.Cycler'>,
  527. ...,
  528. 'namespace': <class 'jinja2.utils.Namespace'>},
  529. 'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd',
  530. ..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'],
  531. 'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined',
  532. ..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']}
  533. .. versionadded:: 2.11.0
  534. """
  535. tags = {"debug"}
  536. def parse(self, parser: "Parser") -> nodes.Output:
  537. lineno = parser.stream.expect("name:debug").lineno
  538. context = nodes.ContextReference()
  539. result = self.call_method("_render", [context], lineno=lineno)
  540. return nodes.Output([result], lineno=lineno)
  541. def _render(self, context: Context) -> str:
  542. result = {
  543. "context": context.get_all(),
  544. "filters": sorted(self.environment.filters.keys()),
  545. "tests": sorted(self.environment.tests.keys()),
  546. }
  547. # Set the depth since the intent is to show the top few names.
  548. return pprint.pformat(result, depth=3, compact=True)
  549. def extract_from_ast(
  550. ast: nodes.Template,
  551. gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
  552. babel_style: bool = True,
  553. ) -> t.Iterator[
  554. t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
  555. ]:
  556. """Extract localizable strings from the given template node. Per
  557. default this function returns matches in babel style that means non string
  558. parameters as well as keyword arguments are returned as `None`. This
  559. allows Babel to figure out what you really meant if you are using
  560. gettext functions that allow keyword arguments for placeholder expansion.
  561. If you don't want that behavior set the `babel_style` parameter to `False`
  562. which causes only strings to be returned and parameters are always stored
  563. in tuples. As a consequence invalid gettext calls (calls without a single
  564. string parameter or string parameters after non-string parameters) are
  565. skipped.
  566. This example explains the behavior:
  567. >>> from jinja2 import Environment
  568. >>> env = Environment()
  569. >>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
  570. >>> list(extract_from_ast(node))
  571. [(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
  572. >>> list(extract_from_ast(node, babel_style=False))
  573. [(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
  574. For every string found this function yields a ``(lineno, function,
  575. message)`` tuple, where:
  576. * ``lineno`` is the number of the line on which the string was found,
  577. * ``function`` is the name of the ``gettext`` function used (if the
  578. string was extracted from embedded Python code), and
  579. * ``message`` is the string, or a tuple of strings for functions
  580. with multiple string arguments.
  581. This extraction function operates on the AST and is because of that unable
  582. to extract any comments. For comment support you have to use the babel
  583. extraction interface or extract comments yourself.
  584. """
  585. out: t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]
  586. for node in ast.find_all(nodes.Call):
  587. if (
  588. not isinstance(node.node, nodes.Name)
  589. or node.node.name not in gettext_functions
  590. ):
  591. continue
  592. strings: t.List[t.Optional[str]] = []
  593. for arg in node.args:
  594. if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
  595. strings.append(arg.value)
  596. else:
  597. strings.append(None)
  598. for _ in node.kwargs:
  599. strings.append(None)
  600. if node.dyn_args is not None:
  601. strings.append(None)
  602. if node.dyn_kwargs is not None:
  603. strings.append(None)
  604. if not babel_style:
  605. out = tuple(x for x in strings if x is not None)
  606. if not out:
  607. continue
  608. else:
  609. if len(strings) == 1:
  610. out = strings[0]
  611. else:
  612. out = tuple(strings)
  613. yield node.lineno, node.node.name, out
  614. class _CommentFinder:
  615. """Helper class to find comments in a token stream. Can only
  616. find comments for gettext calls forwards. Once the comment
  617. from line 4 is found, a comment for line 1 will not return a
  618. usable value.
  619. """
  620. def __init__(
  621. self, tokens: t.Sequence[t.Tuple[int, str, str]], comment_tags: t.Sequence[str]
  622. ) -> None:
  623. self.tokens = tokens
  624. self.comment_tags = comment_tags
  625. self.offset = 0
  626. self.last_lineno = 0
  627. def find_backwards(self, offset: int) -> t.List[str]:
  628. try:
  629. for _, token_type, token_value in reversed(
  630. self.tokens[self.offset : offset]
  631. ):
  632. if token_type in ("comment", "linecomment"):
  633. try:
  634. prefix, comment = token_value.split(None, 1)
  635. except ValueError:
  636. continue
  637. if prefix in self.comment_tags:
  638. return [comment.rstrip()]
  639. return []
  640. finally:
  641. self.offset = offset
  642. def find_comments(self, lineno: int) -> t.List[str]:
  643. if not self.comment_tags or self.last_lineno > lineno:
  644. return []
  645. for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]):
  646. if token_lineno > lineno:
  647. return self.find_backwards(self.offset + idx)
  648. return self.find_backwards(len(self.tokens))
  649. def babel_extract(
  650. fileobj: t.BinaryIO,
  651. keywords: t.Sequence[str],
  652. comment_tags: t.Sequence[str],
  653. options: t.Dict[str, t.Any],
  654. ) -> t.Iterator[
  655. t.Tuple[
  656. int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]], t.List[str]
  657. ]
  658. ]:
  659. """Babel extraction method for Jinja templates.
  660. .. versionchanged:: 2.3
  661. Basic support for translation comments was added. If `comment_tags`
  662. is now set to a list of keywords for extraction, the extractor will
  663. try to find the best preceding comment that begins with one of the
  664. keywords. For best results, make sure to not have more than one
  665. gettext call in one line of code and the matching comment in the
  666. same line or the line before.
  667. .. versionchanged:: 2.5.1
  668. The `newstyle_gettext` flag can be set to `True` to enable newstyle
  669. gettext calls.
  670. .. versionchanged:: 2.7
  671. A `silent` option can now be provided. If set to `False` template
  672. syntax errors are propagated instead of being ignored.
  673. :param fileobj: the file-like object the messages should be extracted from
  674. :param keywords: a list of keywords (i.e. function names) that should be
  675. recognized as translation functions
  676. :param comment_tags: a list of translator tags to search for and include
  677. in the results.
  678. :param options: a dictionary of additional options (optional)
  679. :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
  680. (comments will be empty currently)
  681. """
  682. extensions: t.Dict[t.Type[Extension], None] = {}
  683. for extension_name in options.get("extensions", "").split(","):
  684. extension_name = extension_name.strip()
  685. if not extension_name:
  686. continue
  687. extensions[import_string(extension_name)] = None
  688. if InternationalizationExtension not in extensions:
  689. extensions[InternationalizationExtension] = None
  690. def getbool(options: t.Mapping[str, str], key: str, default: bool = False) -> bool:
  691. return options.get(key, str(default)).lower() in {"1", "on", "yes", "true"}
  692. silent = getbool(options, "silent", True)
  693. environment = Environment(
  694. options.get("block_start_string", defaults.BLOCK_START_STRING),
  695. options.get("block_end_string", defaults.BLOCK_END_STRING),
  696. options.get("variable_start_string", defaults.VARIABLE_START_STRING),
  697. options.get("variable_end_string", defaults.VARIABLE_END_STRING),
  698. options.get("comment_start_string", defaults.COMMENT_START_STRING),
  699. options.get("comment_end_string", defaults.COMMENT_END_STRING),
  700. options.get("line_statement_prefix") or defaults.LINE_STATEMENT_PREFIX,
  701. options.get("line_comment_prefix") or defaults.LINE_COMMENT_PREFIX,
  702. getbool(options, "trim_blocks", defaults.TRIM_BLOCKS),
  703. getbool(options, "lstrip_blocks", defaults.LSTRIP_BLOCKS),
  704. defaults.NEWLINE_SEQUENCE,
  705. getbool(options, "keep_trailing_newline", defaults.KEEP_TRAILING_NEWLINE),
  706. tuple(extensions),
  707. cache_size=0,
  708. auto_reload=False,
  709. )
  710. if getbool(options, "trimmed"):
  711. environment.policies["ext.i18n.trimmed"] = True
  712. if getbool(options, "newstyle_gettext"):
  713. environment.newstyle_gettext = True # type: ignore
  714. source = fileobj.read().decode(options.get("encoding", "utf-8"))
  715. try:
  716. node = environment.parse(source)
  717. tokens = list(environment.lex(environment.preprocess(source)))
  718. except TemplateSyntaxError:
  719. if not silent:
  720. raise
  721. # skip templates with syntax errors
  722. return
  723. finder = _CommentFinder(tokens, comment_tags)
  724. for lineno, func, message in extract_from_ast(node, keywords):
  725. yield lineno, func, message, finder.find_comments(lineno)
  726. #: nicer import names
  727. i18n = InternationalizationExtension
  728. do = ExprStmtExtension
  729. loopcontrols = LoopControlExtension
  730. with_ = WithExtension
  731. autoescape = AutoEscapeExtension
  732. debug = DebugExtension