Mercurial > piecrust2
comparison piecrust/rendering.py @ 3:f485ba500df3
Gigantic change to basically make PieCrust 2 vaguely functional.
- Serving works, with debug window.
- Baking works, multi-threading, with dependency handling.
- Various things not implemented yet.
| author | Ludovic Chabant <ludovic@chabant.com> |
|---|---|
| date | Sun, 10 Aug 2014 23:43:16 -0700 |
| parents | |
| children | 474c9882decf |
comparison
equal
deleted
inserted
replaced
| 2:40fa08b261b9 | 3:f485ba500df3 |
|---|---|
| 1 import re | |
| 2 import os.path | |
| 3 import codecs | |
| 4 import logging | |
| 5 from piecrust.data.builder import (DataBuildingContext, build_page_data, | |
| 6 build_layout_data) | |
| 7 from piecrust.environment import PHASE_PAGE_FORMATTING, PHASE_PAGE_RENDERING | |
| 8 | |
| 9 | |
| 10 logger = logging.getLogger(__name__) | |
| 11 | |
| 12 | |
| 13 content_abstract_re = re.compile(r'^<!--\s*(more|(page)?break)\s*-->\s*$', | |
| 14 re.MULTILINE) | |
| 15 | |
| 16 | |
| 17 class PageRenderingError(Exception): | |
| 18 pass | |
| 19 | |
| 20 | |
| 21 class RenderedPage(object): | |
| 22 def __init__(self, page, uri, num=1): | |
| 23 self.page = page | |
| 24 self.uri = uri | |
| 25 self.num = num | |
| 26 self.data = None | |
| 27 self.content = None | |
| 28 self.execution_info = None | |
| 29 | |
| 30 @property | |
| 31 def app(self): | |
| 32 return self.page.app | |
| 33 | |
| 34 | |
| 35 class PageRenderingContext(object): | |
| 36 def __init__(self, page, uri, page_num=1): | |
| 37 self.page = page | |
| 38 self.uri = uri | |
| 39 self.page_num = page_num | |
| 40 self.pagination_source = None | |
| 41 self.pagination_filter = None | |
| 42 self.custom_data = None | |
| 43 self.use_cache = False | |
| 44 self.used_pagination = None | |
| 45 self.used_source_names = set() | |
| 46 self.used_taxonomy_terms = set() | |
| 47 | |
| 48 @property | |
| 49 def app(self): | |
| 50 return self.page.app | |
| 51 | |
| 52 @property | |
| 53 def source_metadata(self): | |
| 54 return self.page.source_metadata | |
| 55 | |
| 56 def reset(self): | |
| 57 self.used_pagination = None | |
| 58 | |
| 59 def setPagination(self, paginator): | |
| 60 if self.used_pagination is not None: | |
| 61 raise Exception("Pagination has already been used.") | |
| 62 self.used_pagination = paginator | |
| 63 | |
| 64 | |
| 65 def render_page(ctx): | |
| 66 eis = ctx.app.env.exec_info_stack | |
| 67 eis.pushPage(ctx.page, PHASE_PAGE_RENDERING, ctx) | |
| 68 try: | |
| 69 page = ctx.page | |
| 70 | |
| 71 # Build the data for both segment and layout rendering. | |
| 72 data_ctx = DataBuildingContext(page, ctx.uri, ctx.page_num) | |
| 73 data_ctx.pagination_source = ctx.pagination_source | |
| 74 data_ctx.pagination_filter = ctx.pagination_filter | |
| 75 page_data = build_page_data(data_ctx) | |
| 76 if ctx.custom_data: | |
| 77 page_data.update(ctx.custom_data) | |
| 78 | |
| 79 # Render content segments. | |
| 80 repo = ctx.app.env.rendered_segments_repository | |
| 81 if repo: | |
| 82 cache_key = '%s:%s' % (ctx.uri, ctx.page_num) | |
| 83 contents = repo.get(cache_key, | |
| 84 lambda: _do_render_page_segments(page, page_data)) | |
| 85 else: | |
| 86 contents = _do_render_page_segments(page, page_data) | |
| 87 | |
| 88 # Render layout. | |
| 89 layout_name = page.config.get('layout') | |
| 90 if layout_name is None: | |
| 91 layout_name = page.source.config.get('default_layout', 'default') | |
| 92 null_names = ['', 'none', 'nil'] | |
| 93 if layout_name not in null_names: | |
| 94 layout_data = build_layout_data(page, page_data, contents) | |
| 95 output = render_layout(layout_name, page, layout_data) | |
| 96 else: | |
| 97 output = contents['content'] | |
| 98 | |
| 99 rp = RenderedPage(page, ctx.uri, ctx.page_num) | |
| 100 rp.data = page_data | |
| 101 rp.content = codecs.encode(output, 'utf8') | |
| 102 rp.execution_info = eis.current_page_info | |
| 103 return rp | |
| 104 finally: | |
| 105 eis.popPage() | |
| 106 | |
| 107 | |
| 108 def render_page_segments(ctx): | |
| 109 repo = ctx.app.env.rendered_segments_repository | |
| 110 if repo: | |
| 111 cache_key = '%s:%s' % (ctx.uri, ctx.page_num) | |
| 112 return repo.get(cache_key, | |
| 113 lambda: _do_render_page_segments_from_ctx(ctx)) | |
| 114 | |
| 115 return _do_render_page_segments_from_ctx(ctx) | |
| 116 | |
| 117 | |
| 118 def _do_render_page_segments_from_ctx(ctx): | |
| 119 eis = ctx.app.env.exec_info_stack | |
| 120 eis.pushPage(ctx.page, PHASE_PAGE_FORMATTING, ctx) | |
| 121 try: | |
| 122 data_ctx = DataBuildingContext(ctx.page, ctx.uri, ctx.page_num) | |
| 123 page_data = build_page_data(data_ctx) | |
| 124 return _do_render_page_segments(ctx.page, page_data) | |
| 125 finally: | |
| 126 eis.popPage() | |
| 127 | |
| 128 | |
| 129 def _do_render_page_segments(page, page_data): | |
| 130 app = page.app | |
| 131 engine_name = page.config.get('template_engine') | |
| 132 format_name = page.config.get('format') | |
| 133 | |
| 134 engine = get_template_engine(app, engine_name) | |
| 135 if engine is None: | |
| 136 raise PageRenderingError("Can't find template engine '%s'." % engine_name) | |
| 137 | |
| 138 formatted_content = {} | |
| 139 for seg_name, seg in page.raw_content.iteritems(): | |
| 140 seg_text = u'' | |
| 141 for seg_part in seg.parts: | |
| 142 part_format = seg_part.fmt or format_name | |
| 143 part_text = engine.renderString(seg_part.content, page_data, | |
| 144 filename=page.path, line_offset=seg_part.line) | |
| 145 part_text = format_text(app, part_format, part_text) | |
| 146 seg_text += part_text | |
| 147 formatted_content[seg_name] = seg_text | |
| 148 | |
| 149 if seg_name == 'content': | |
| 150 m = content_abstract_re.search(seg_text) | |
| 151 if m: | |
| 152 offset = m.start() | |
| 153 content_abstract = seg_text[:offset] | |
| 154 formatted_content['content.abstract'] = content_abstract | |
| 155 | |
| 156 return formatted_content | |
| 157 | |
| 158 | |
| 159 def render_layout(layout_name, page, layout_data): | |
| 160 names = layout_name.split(',') | |
| 161 default_template_engine = get_template_engine(page.app, None) | |
| 162 default_exts = ['.' + e.lstrip('.') for e in default_template_engine.EXTENSIONS] | |
| 163 full_names = [] | |
| 164 for name in names: | |
| 165 if '.' not in name: | |
| 166 full_names.append(name + '.html') | |
| 167 for ext in default_exts: | |
| 168 full_names.append(name + ext) | |
| 169 else: | |
| 170 full_names.append(name) | |
| 171 | |
| 172 _, engine_name = os.path.splitext(full_names[0]) | |
| 173 engine_name = engine_name.lstrip('.') | |
| 174 engine = get_template_engine(page.app, engine_name) | |
| 175 if engine is None: | |
| 176 raise PageRenderingError("No such template engine: %s" % engine_name) | |
| 177 output = engine.renderFile(full_names, layout_data) | |
| 178 return output | |
| 179 | |
| 180 | |
| 181 def get_template_engine(app, engine_name): | |
| 182 if engine_name == 'html': | |
| 183 engine_name = None | |
| 184 engine_name = engine_name or app.config.get('site/default_template_engine') | |
| 185 for engine in app.plugin_loader.getTemplateEngines(): | |
| 186 if engine_name in engine.ENGINE_NAMES: | |
| 187 return engine | |
| 188 return None | |
| 189 | |
| 190 def format_text(app, format_name, txt): | |
| 191 format_name = format_name or app.config.get('site/default_format') | |
| 192 for fmt in app.plugin_loader.getFormatters(): | |
| 193 if fmt.FORMAT_NAMES is None or format_name in fmt.FORMAT_NAMES: | |
| 194 txt = fmt.render(format_name, txt) | |
| 195 if fmt.OUTPUT_FORMAT is not None: | |
| 196 format_name = fmt.OUTPUT_FORMAT | |
| 197 return txt | |
| 198 |
