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

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