ext.py 31 KB

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