A fork of hyde, the static site generation. Some patches will be pushed upstream.
 
 
 

811 lines
25 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Jinja template utilties
  4. """
  5. from datetime import datetime, date
  6. import os
  7. import re
  8. import itertools
  9. from urllib import quote, unquote
  10. from hyde.fs import File, Folder
  11. from hyde.model import Expando
  12. from hyde.template import HtmlWrap, Template
  13. from hyde.util import getLoggerWithNullHandler
  14. from operator import attrgetter
  15. from jinja2 import contextfunction, Environment
  16. from jinja2 import FileSystemLoader, FileSystemBytecodeCache
  17. from jinja2 import contextfilter, environmentfilter, Markup, Undefined, nodes
  18. from jinja2.ext import Extension
  19. from jinja2.exceptions import TemplateError
  20. logger = getLoggerWithNullHandler('hyde.engine.Jinja2')
  21. class SilentUndefined(Undefined):
  22. """
  23. A redefinition of undefined that eats errors.
  24. """
  25. def __getattr__(self, name):
  26. return self
  27. __getitem__ = __getattr__
  28. def __call__(self, *args, **kwargs):
  29. return self
  30. @contextfunction
  31. def media_url(context, path):
  32. """
  33. Returns the media url given a partial path.
  34. """
  35. return context['site'].media_url(path)
  36. @contextfunction
  37. def content_url(context, path):
  38. """
  39. Returns the content url given a partial path.
  40. """
  41. return context['site'].content_url(path)
  42. @contextfunction
  43. def full_url(context, path):
  44. """
  45. Returns the full url given a partial path.
  46. """
  47. return context['site'].full_url(path)
  48. @contextfilter
  49. def urlencode(ctx, url):
  50. return quote(url.encode('utf8'))
  51. @contextfilter
  52. def urldecode(ctx, url):
  53. return unquote(url).decode('utf8')
  54. @contextfilter
  55. def date_format(ctx, dt, fmt=None):
  56. if not dt:
  57. dt = datetime.now()
  58. if not isinstance(dt, datetime) or \
  59. not isinstance(dt, date):
  60. logger.error("Date format called on a non date object")
  61. return dt
  62. format = fmt or "%a, %d %b %Y"
  63. if not fmt:
  64. global_format = ctx.resolve('dateformat')
  65. if not isinstance(global_format, Undefined):
  66. format = global_format
  67. return dt.strftime(format)
  68. def islice(iterable, start=0, stop=3, step=1):
  69. return itertools.islice(iterable, start, stop, step)
  70. def top(iterable, count=3):
  71. return islice(iterable, stop=count)
  72. def xmldatetime(dt):
  73. if not dt:
  74. dt = datetime.now()
  75. zprefix = "Z"
  76. tz = dt.strftime("%z")
  77. if tz:
  78. zprefix = tz[:3] + ":" + tz[3:]
  79. return dt.strftime("%Y-%m-%dT%H:%M:%S") + zprefix
  80. @environmentfilter
  81. def asciidoc(env, value):
  82. """
  83. (simple) Asciidoc filter
  84. """
  85. try:
  86. from asciidocapi import AsciiDocAPI
  87. except ImportError:
  88. print u"Requires AsciiDoc library to use AsciiDoc tag."
  89. raise
  90. import StringIO
  91. output = value
  92. asciidoc = AsciiDocAPI()
  93. asciidoc.options('--no-header-footer')
  94. result = StringIO.StringIO()
  95. asciidoc.execute(StringIO.StringIO(output.encode('utf-8')), result, backend='html4')
  96. return unicode(result.getvalue(), "utf-8")
  97. @environmentfilter
  98. def markdown(env, value):
  99. """
  100. Markdown filter with support for extensions.
  101. """
  102. try:
  103. import markdown as md
  104. except ImportError:
  105. logger.error(u"Cannot load the markdown library.")
  106. raise TemplateError(u"Cannot load the markdown library")
  107. output = value
  108. d = {}
  109. if hasattr(env.config, 'markdown'):
  110. d['extensions'] = getattr(env.config.markdown, 'extensions', [])
  111. d['extension_configs'] = getattr(env.config.markdown,
  112. 'extension_configs',
  113. Expando({})).to_dict()
  114. if hasattr(env.config.markdown, 'output_format'):
  115. d['output_format'] = env.config.markdown.output_format
  116. marked = md.Markdown(**d)
  117. return marked.convert(output)
  118. @environmentfilter
  119. def restructuredtext(env, value):
  120. """
  121. RestructuredText filter
  122. """
  123. try:
  124. from docutils.core import publish_parts
  125. except ImportError:
  126. logger.error(u"Cannot load the docutils library.")
  127. raise TemplateError(u"Cannot load the docutils library.")
  128. highlight_source = False
  129. if hasattr(env.config, 'restructuredtext'):
  130. highlight_source = getattr(env.config.restructuredtext, 'highlight_source', False)
  131. if highlight_source:
  132. import hyde.lib.pygments.rst_directive
  133. parts = publish_parts(source=value, writer_name="html")
  134. return parts['html_body']
  135. @environmentfilter
  136. def syntax(env, value, lexer=None, filename=None):
  137. """
  138. Processes the contained block using `pygments`
  139. """
  140. try:
  141. import pygments
  142. from pygments import lexers
  143. from pygments import formatters
  144. except ImportError:
  145. logger.error(u"pygments library is required to"
  146. " use syntax highlighting tags.")
  147. raise TemplateError("Cannot load pygments")
  148. pyg = (lexers.get_lexer_by_name(lexer)
  149. if lexer else
  150. lexers.guess_lexer(value))
  151. settings = {}
  152. if hasattr(env.config, 'syntax'):
  153. settings = getattr(env.config.syntax,
  154. 'options',
  155. Expando({})).to_dict()
  156. formatter = formatters.HtmlFormatter(**settings)
  157. code = pygments.highlight(value, pyg, formatter)
  158. code = code.replace('\n\n', '\n&nbsp;\n').replace('\n', '<br />')
  159. caption = filename if filename else pyg.name
  160. if hasattr(env.config, 'syntax'):
  161. if not getattr(env.config.syntax, 'use_figure', True):
  162. return Markup(code)
  163. return Markup(
  164. '<div class="codebox"><figure class="code">%s<figcaption>%s</figcaption></figure></div>\n\n'
  165. % (code, caption))
  166. class Spaceless(Extension):
  167. """
  168. Emulates the django spaceless template tag.
  169. """
  170. tags = set(['spaceless'])
  171. def parse(self, parser):
  172. """
  173. Parses the statements and calls back to strip spaces.
  174. """
  175. lineno = parser.stream.next().lineno
  176. body = parser.parse_statements(['name:endspaceless'],
  177. drop_needle=True)
  178. return nodes.CallBlock(
  179. self.call_method('_render_spaceless'),
  180. [], [], body).set_lineno(lineno)
  181. def _render_spaceless(self, caller=None):
  182. """
  183. Strip the spaces between tags using the regular expression
  184. from django. Stolen from `django.util.html` Returns the given HTML
  185. with spaces between tags removed.
  186. """
  187. if not caller:
  188. return ''
  189. return re.sub(r'>\s+<', '><', unicode(caller().strip()))
  190. class Asciidoc(Extension):
  191. """
  192. A wrapper around the asciidoc filter for syntactic sugar.
  193. """
  194. tags = set(['asciidoc'])
  195. def parse(self, parser):
  196. """
  197. Parses the statements and defers to the callback for asciidoc processing.
  198. """
  199. lineno = parser.stream.next().lineno
  200. body = parser.parse_statements(['name:endasciidoc'], drop_needle=True)
  201. return nodes.CallBlock(
  202. self.call_method('_render_asciidoc'),
  203. [], [], body).set_lineno(lineno)
  204. def _render_asciidoc(self, caller=None):
  205. """
  206. Calls the asciidoc filter to transform the output.
  207. """
  208. if not caller:
  209. return ''
  210. output = caller().strip()
  211. return asciidoc(self.environment, output)
  212. class Markdown(Extension):
  213. """
  214. A wrapper around the markdown filter for syntactic sugar.
  215. """
  216. tags = set(['markdown'])
  217. def parse(self, parser):
  218. """
  219. Parses the statements and defers to the callback for markdown processing.
  220. """
  221. lineno = parser.stream.next().lineno
  222. body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)
  223. return nodes.CallBlock(
  224. self.call_method('_render_markdown'),
  225. [], [], body).set_lineno(lineno)
  226. def _render_markdown(self, caller=None):
  227. """
  228. Calls the markdown filter to transform the output.
  229. """
  230. if not caller:
  231. return ''
  232. output = caller().strip()
  233. return markdown(self.environment, output)
  234. class restructuredText(Extension):
  235. """
  236. A wrapper around the restructuredtext filter for syntactic sugar
  237. """
  238. tags = set(['restructuredtext'])
  239. def parse(self, parser):
  240. """
  241. Simply extract our content
  242. """
  243. lineno = parser.stream.next().lineno
  244. body = parser.parse_statements(['name:endrestructuredtext'], drop_needle=True)
  245. return nodes.CallBlock(self.call_method('_render_rst'), [], [], body
  246. ).set_lineno(lineno)
  247. def _render_rst(self, caller=None):
  248. """
  249. call our restructuredtext filter
  250. """
  251. if not caller:
  252. return ''
  253. output = caller().strip()
  254. return restructuredtext(self.environment, output)
  255. class YamlVar(Extension):
  256. """
  257. An extension that converts the content between the tags
  258. into an yaml object and sets the value in the given
  259. variable.
  260. """
  261. tags = set(['yaml'])
  262. def parse(self, parser):
  263. """
  264. Parses the contained data and defers to the callback to load it as
  265. yaml.
  266. """
  267. lineno = parser.stream.next().lineno
  268. var = parser.stream.expect('name').value
  269. body = parser.parse_statements(['name:endyaml'], drop_needle=True)
  270. return [
  271. nodes.Assign(
  272. nodes.Name(var, 'store'),
  273. nodes.Const({})
  274. ).set_lineno(lineno),
  275. nodes.CallBlock(
  276. self.call_method('_set_yaml',
  277. args=[nodes.Name(var, 'load')]),
  278. [], [], body).set_lineno(lineno)
  279. ]
  280. def _set_yaml(self, var, caller=None):
  281. """
  282. Loads the yaml data into the specified variable.
  283. """
  284. if not caller:
  285. return ''
  286. try:
  287. import yaml
  288. except ImportError:
  289. return ''
  290. out = caller().strip()
  291. var.update(yaml.load(out))
  292. return ''
  293. def parse_kwargs(parser):
  294. """
  295. Parses keyword arguments in tags.
  296. """
  297. name = parser.stream.expect('name').value
  298. parser.stream.expect('assign')
  299. if parser.stream.current.test('string'):
  300. value = parser.parse_expression()
  301. else:
  302. value = nodes.Const(parser.stream.next().value)
  303. return (name, value)
  304. class Syntax(Extension):
  305. """
  306. A wrapper around the syntax filter for syntactic sugar.
  307. """
  308. tags = set(['syntax'])
  309. def parse(self, parser):
  310. """
  311. Parses the statements and defers to the callback for pygments processing.
  312. """
  313. lineno = parser.stream.next().lineno
  314. lex = nodes.Const(None)
  315. filename = nodes.Const(None)
  316. if not parser.stream.current.test('block_end'):
  317. if parser.stream.look().test('assign'):
  318. name = value = value1 = None
  319. (name, value) = parse_kwargs(parser)
  320. if parser.stream.skip_if('comma'):
  321. (_, value1) = parse_kwargs(parser)
  322. (lex, filename) = (value, value1) \
  323. if name == 'lex' \
  324. else (value1, value)
  325. else:
  326. lex = nodes.Const(parser.stream.next().value)
  327. if parser.stream.skip_if('comma'):
  328. filename = parser.parse_expression()
  329. body = parser.parse_statements(['name:endsyntax'], drop_needle=True)
  330. return nodes.CallBlock(
  331. self.call_method('_render_syntax',
  332. args=[lex, filename]),
  333. [], [], body).set_lineno(lineno)
  334. def _render_syntax(self, lex, filename, caller=None):
  335. """
  336. Calls the syntax filter to transform the output.
  337. """
  338. if not caller:
  339. return ''
  340. output = caller().strip()
  341. return syntax(self.environment, output, lex, filename)
  342. class IncludeText(Extension):
  343. """
  344. Automatically runs `markdown` and `typogrify` on included
  345. files.
  346. """
  347. tags = set(['includetext'])
  348. def parse(self, parser):
  349. """
  350. Delegates all the parsing to the native include node.
  351. """
  352. node = parser.parse_include()
  353. return nodes.CallBlock(
  354. self.call_method('_render_include_text'),
  355. [], [], [node]).set_lineno(node.lineno)
  356. def _render_include_text(self, caller=None):
  357. """
  358. Runs markdown and if available, typogrigy on the
  359. content returned by the include node.
  360. """
  361. if not caller:
  362. return ''
  363. output = caller().strip()
  364. output = markdown(self.environment, output)
  365. if 'typogrify' in self.environment.filters:
  366. typo = self.environment.filters['typogrify']
  367. output = typo(output)
  368. return output
  369. MARKINGS = '_markings_'
  370. class Reference(Extension):
  371. """
  372. Marks a block in a template such that its available for use
  373. when referenced using a `refer` tag.
  374. """
  375. tags = set(['mark', 'reference'])
  376. def parse(self, parser):
  377. """
  378. Parse the variable name that the content must be assigned to.
  379. """
  380. token = parser.stream.next()
  381. lineno = token.lineno
  382. tag = token.value
  383. name = parser.stream.next().value
  384. body = parser.parse_statements(['name:end%s' % tag], drop_needle=True)
  385. return nodes.CallBlock(
  386. self.call_method('_render_output',
  387. args=[nodes.Name(MARKINGS, 'load'), nodes.Const(name)]),
  388. [], [], body).set_lineno(lineno)
  389. def _render_output(self, markings, name, caller=None):
  390. """
  391. Assigns the result of the contents to the markings variable.
  392. """
  393. if not caller:
  394. return ''
  395. out = caller()
  396. if isinstance(markings, dict):
  397. markings[name] = out
  398. return out
  399. class Refer(Extension):
  400. """
  401. Imports content blocks specified in the referred template as
  402. variables in a given namespace.
  403. """
  404. tags = set(['refer'])
  405. def parse(self, parser):
  406. """
  407. Parse the referred template and the namespace.
  408. """
  409. token = parser.stream.next()
  410. lineno = token.lineno
  411. parser.stream.expect('name:to')
  412. template = parser.parse_expression()
  413. parser.stream.expect('name:as')
  414. namespace = parser.stream.next().value
  415. includeNode = nodes.Include(lineno=lineno)
  416. includeNode.with_context = True
  417. includeNode.ignore_missing = False
  418. includeNode.template = template
  419. temp = parser.free_identifier(lineno)
  420. return [
  421. nodes.Assign(
  422. nodes.Name(temp.name, 'store'),
  423. nodes.Name(MARKINGS, 'load')
  424. ).set_lineno(lineno),
  425. nodes.Assign(
  426. nodes.Name(MARKINGS, 'store'),
  427. nodes.Const({})).set_lineno(lineno),
  428. nodes.Assign(
  429. nodes.Name(namespace, 'store'),
  430. nodes.Const({})).set_lineno(lineno),
  431. nodes.CallBlock(
  432. self.call_method('_push_resource',
  433. args=[
  434. nodes.Name(namespace, 'load'),
  435. nodes.Name('site', 'load'),
  436. nodes.Name('resource', 'load'),
  437. template]),
  438. [], [], []).set_lineno(lineno),
  439. nodes.Assign(
  440. nodes.Name('resource', 'store'),
  441. nodes.Getitem(nodes.Name(namespace, 'load'),
  442. nodes.Const('resource'), 'load')
  443. ).set_lineno(lineno),
  444. nodes.CallBlock(
  445. self.call_method('_assign_reference',
  446. args=[
  447. nodes.Name(MARKINGS, 'load'),
  448. nodes.Name(namespace, 'load')]),
  449. [], [], [includeNode]).set_lineno(lineno),
  450. nodes.Assign(nodes.Name('resource', 'store'),
  451. nodes.Getitem(nodes.Name(namespace, 'load'),
  452. nodes.Const('parent_resource'), 'load')
  453. ).set_lineno(lineno),
  454. nodes.Assign(
  455. nodes.Name(MARKINGS, 'store'),
  456. nodes.Name(temp.name, 'load')
  457. ).set_lineno(lineno),
  458. ]
  459. def _push_resource(self, namespace, site, resource, template, caller):
  460. """
  461. Saves the current references in a stack.
  462. """
  463. namespace['parent_resource'] = resource
  464. if not hasattr(resource, 'depends'):
  465. resource.depends = []
  466. if not template in resource.depends:
  467. resource.depends.append(template)
  468. namespace['resource'] = site.content.resource_from_relative_path(
  469. template)
  470. return ''
  471. def _assign_reference(self, markings, namespace, caller):
  472. """
  473. Assign the processed variables into the
  474. given namespace.
  475. """
  476. out = caller()
  477. for key, value in markings.items():
  478. namespace[key] = value
  479. namespace['html'] = HtmlWrap(out)
  480. return ''
  481. class HydeLoader(FileSystemLoader):
  482. """
  483. A wrapper around the file system loader that performs
  484. hyde specific tweaks.
  485. """
  486. def __init__(self, sitepath, site, preprocessor=None):
  487. config = site.config if hasattr(site, 'config') else None
  488. if config:
  489. super(HydeLoader, self).__init__([
  490. unicode(config.content_root_path),
  491. unicode(config.layout_root_path),
  492. ])
  493. else:
  494. super(HydeLoader, self).__init__(unicode(sitepath))
  495. self.site = site
  496. self.preprocessor = preprocessor
  497. def get_source(self, environment, template):
  498. """
  499. Calls the plugins to preprocess prior to returning the source.
  500. """
  501. template = template.strip()
  502. # Fixed so that jinja2 loader does not have issues with
  503. # seprator in windows
  504. #
  505. template = template.replace(os.sep, '/')
  506. logger.debug("Loading template [%s] and preprocessing" % template)
  507. (contents,
  508. filename,
  509. date) = super(HydeLoader, self).get_source(
  510. environment, template)
  511. if self.preprocessor:
  512. resource = self.site.content.resource_from_relative_path(template)
  513. if resource:
  514. contents = self.preprocessor(resource, contents) or contents
  515. return (contents, filename, date)
  516. # pylint: disable-msg=W0104,E0602,W0613,R0201
  517. class Jinja2Template(Template):
  518. """
  519. The Jinja2 Template implementation
  520. """
  521. def __init__(self, sitepath):
  522. super(Jinja2Template, self).__init__(sitepath)
  523. def configure(self, site, engine=None):
  524. """
  525. Uses the site object to initialize the jinja environment.
  526. """
  527. self.site = site
  528. self.engine = engine
  529. self.preprocessor = (engine.preprocessor
  530. if hasattr(engine, 'preprocessor') else None)
  531. self.loader = HydeLoader(self.sitepath, site, self.preprocessor)
  532. default_extensions = [
  533. IncludeText,
  534. Spaceless,
  535. Asciidoc,
  536. Markdown,
  537. restructuredText,
  538. Syntax,
  539. Reference,
  540. Refer,
  541. YamlVar,
  542. 'jinja2.ext.do',
  543. 'jinja2.ext.loopcontrols',
  544. 'jinja2.ext.with_'
  545. ]
  546. defaults = {
  547. 'line_statement_prefix': '$$$',
  548. 'trim_blocks': True,
  549. }
  550. settings = dict()
  551. settings.update(defaults)
  552. settings['extensions'] = list()
  553. settings['extensions'].extend(default_extensions)
  554. conf = {}
  555. try:
  556. conf = attrgetter('config.jinja2')(site).to_dict()
  557. except AttributeError:
  558. pass
  559. settings.update(
  560. dict([(key, conf[key]) for key in defaults if key in conf]))
  561. extensions = conf.get('extensions', [])
  562. if isinstance(extensions, list):
  563. settings['extensions'].extend(extensions)
  564. else:
  565. settings['extensions'].append(extensions)
  566. self.env = Environment(
  567. loader=self.loader,
  568. undefined=SilentUndefined,
  569. line_statement_prefix=settings['line_statement_prefix'],
  570. trim_blocks=True,
  571. bytecode_cache=FileSystemBytecodeCache(),
  572. extensions=settings['extensions'])
  573. self.env.globals['media_url'] = media_url
  574. self.env.globals['content_url'] = content_url
  575. self.env.globals['full_url'] = full_url
  576. self.env.globals['engine'] = engine
  577. self.env.globals['deps'] = {}
  578. self.env.filters['urlencode'] = urlencode
  579. self.env.filters['urldecode'] = urldecode
  580. self.env.filters['asciidoc'] = asciidoc
  581. self.env.filters['markdown'] = markdown
  582. self.env.filters['restructuredtext'] = restructuredtext
  583. self.env.filters['syntax'] = syntax
  584. self.env.filters['date_format'] = date_format
  585. self.env.filters['xmldatetime'] = xmldatetime
  586. self.env.filters['islice'] = islice
  587. self.env.filters['top'] = top
  588. config = {}
  589. if hasattr(site, 'config'):
  590. config = site.config
  591. self.env.extend(config=config)
  592. try:
  593. from typogrify.templatetags import jinja2_filters
  594. except ImportError:
  595. jinja2_filters = False
  596. if jinja2_filters:
  597. jinja2_filters.register(self.env)
  598. def clear_caches(self):
  599. """
  600. Clear all caches to prepare for regeneration
  601. """
  602. if self.env.bytecode_cache:
  603. self.env.bytecode_cache.clear()
  604. def get_dependencies(self, path):
  605. """
  606. Finds dependencies hierarchically based on the included
  607. files.
  608. """
  609. text = self.env.loader.get_source(self.env, path)[0]
  610. from jinja2.meta import find_referenced_templates
  611. try:
  612. ast = self.env.parse(text)
  613. except:
  614. logger.error("Error parsing[%s]" % path)
  615. raise
  616. tpls = find_referenced_templates(ast)
  617. deps = list(self.env.globals['deps'].get('path', []))
  618. for dep in tpls:
  619. deps.append(dep)
  620. if dep:
  621. deps.extend(self.get_dependencies(dep))
  622. return list(set(deps))
  623. @property
  624. def exception_class(self):
  625. """
  626. The exception to throw. Used by plugins.
  627. """
  628. return TemplateError
  629. @property
  630. def patterns(self):
  631. """
  632. The pattern for matching selected template statements.
  633. """
  634. return {
  635. "block_open": '\s*\{\%\s*block\s*([^\s]+)\s*\%\}',
  636. "block_close": '\s*\{\%\s*endblock\s*([^\s]*)\s*\%\}',
  637. "include": '\s*\{\%\s*include\s*(?:\'|\")(.+?\.[^.]*)(?:\'|\")\s*\%\}',
  638. "extends": '\s*\{\%\s*extends\s*(?:\'|\")(.+?\.[^.]*)(?:\'|\")\s*\%\}'
  639. }
  640. def get_include_statement(self, path_to_include):
  641. """
  642. Returns an include statement for the current template,
  643. given the path to include.
  644. """
  645. return '{%% include \'%s\' %%}' % path_to_include
  646. def get_extends_statement(self, path_to_extend):
  647. """
  648. Returns an extends statement for the current template,
  649. given the path to extend.
  650. """
  651. return '{%% extends \'%s\' %%}' % path_to_extend
  652. def get_open_tag(self, tag, params):
  653. """
  654. Returns an open tag statement.
  655. """
  656. return '{%% %s %s %%}' % (tag, params)
  657. def get_close_tag(self, tag, params):
  658. """
  659. Returns an open tag statement.
  660. """
  661. return '{%% end%s %%}' % tag
  662. def get_content_url_statement(self, url):
  663. """
  664. Returns the content url statement.
  665. """
  666. return '{{ content_url(\'%s\') }}' % url
  667. def get_media_url_statement(self, url):
  668. """
  669. Returns the media url statement.
  670. """
  671. return '{{ media_url(\'%s\') }}' % url
  672. def get_full_url_statement(self, url):
  673. """
  674. Returns the full url statement.
  675. """
  676. return '{{ full_url(\'%s\') }}' % url
  677. def render_resource(self, resource, context):
  678. """
  679. Renders the given resource using the context
  680. """
  681. try:
  682. template = self.env.get_template(resource.relative_path)
  683. out = template.render(context)
  684. except:
  685. out = ""
  686. logger.debug(self.env.loader.get_source(
  687. self.env, resource.relative_path))
  688. raise
  689. return out
  690. def render(self, text, context):
  691. """
  692. Renders the given text using the context
  693. """
  694. template = self.env.from_string(text)
  695. return template.render(context)