changeset 924:1bb704434ee2

formatting: Remove segment parts, you can use template tags instead. Segment parts were used to switch formatters insides a given content segment, but that's also achievable with template tags like `pcformat` in Jinja to some degree. It's not totally the same but removing it simplifies the code and improves performance.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 01 Oct 2017 20:36:04 -0700
parents 5713b6a2850d
children 2394fd689590
files piecrust/page.py piecrust/rendering.py piecrust/templating/base.py piecrust/templating/jinja/loader.py piecrust/templating/jinjaengine.py piecrust/templating/pystacheengine.py
diffstat 6 files changed, 76 insertions(+), 116 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/page.py	Fri Sep 29 19:59:19 2017 -0700
+++ b/piecrust/page.py	Sun Oct 01 20:36:04 2017 -0700
@@ -222,32 +222,20 @@
 class ContentSegment(object):
     debug_render_func = 'debug_render'
 
-    def __init__(self):
-        self.parts = []
-
-    def debug_render(self):
-        return '\n'.join([p.content for p in self.parts])
-
-
-class ContentSegmentPart(object):
     def __init__(self, content, fmt=None, offset=-1, line=-1):
         self.content = content
         self.fmt = fmt
         self.offset = offset
         self.line = line
 
-    def __str__(self):
-        return '%s [%s]' % (self.content, self.fmt or '<default>')
+    def debug_render(self):
+        return '[%s] %s' % (self.fmt or '<none>', self.content)
 
 
 def json_load_segments(data):
     segments = {}
-    for key, seg_data in data.items():
-        seg = ContentSegment()
-        for p_data in seg_data:
-            part = ContentSegmentPart(p_data['c'], p_data['f'], p_data['o'],
-                                      p_data['l'])
-            seg.parts.append(part)
+    for key, sd in data.items():
+        seg = ContentSegment(sd['c'], sd['f'], sd['o'], sd['l'])
         segments[key] = seg
     return segments
 
@@ -255,11 +243,8 @@
 def json_save_segments(segments):
     data = {}
     for key, seg in segments.items():
-        seg_data = []
-        for part in seg.parts:
-            p_data = {'c': part.content, 'f': part.fmt, 'o': part.offset,
-                      'l': part.line}
-            seg_data.append(p_data)
+        seg_data = {
+            'c': seg.content, 'f': seg.fmt, 'o': seg.offset, 'l': seg.line}
         data[key] = seg_data
     return data
 
@@ -314,24 +299,33 @@
 segment_pattern = re.compile(
     r"""^\-\-\-\s*(?P<name>\w+)(\:(?P<fmt>\w+))?\s*\-\-\-\s*$""",
     re.M)
-part_pattern = re.compile(
-    r"""^<\-\-\s*(?P<fmt>\w+)\s*\-\->\s*$""",
-    re.M)
 
 
-def _count_lines(s):
-    return len(s.split('\n'))
+def _count_lines(txt, start=0, end=-1):
+    cur = start
+    line_count = 1
+    while True:
+        nex = txt.find('\n', cur)
+        if nex < 0:
+            break
+
+        cur = nex + 1
+        line_count += 1
+
+        if end >= 0 and cur >= end:
+            break
+
+    return line_count
 
 
 def _string_needs_parsing(txt, offset):
     txtlen = len(txt)
     index = txt.find('-', offset)
     while index >= 0 and index < txtlen - 8:
-        # Look for a potential `<--format-->`
-        if index > 0 and txt[index - 1] == '<' and txt[index + 1] == '-':
-            return True
         # Look for a potential `---segment---`
-        if txt[index + 1] == '-' and txt[index + 2] == '-':
+        if (index > 0 and
+                txt[index - 1] == '\n' and
+                txt[index + 1] == '-' and txt[index + 2] == '-'):
             return True
         index = txt.find('-', index + 1)
     return False
@@ -339,18 +333,16 @@
 
 def parse_segments(raw, offset=0):
     # Get the number of lines in the header.
-    header_lines = _count_lines(raw[:offset].rstrip())
+    header_lines = _count_lines(raw, 0, offset)
     current_line = header_lines
 
     # Figure out if we need any parsing.
     do_parse = _string_needs_parsing(raw, offset)
     if not do_parse:
-        seg = ContentSegment()
-        seg.parts = [
-            ContentSegmentPart(raw[offset:], None, offset, current_line)]
+        seg = ContentSegment(raw[offset:], None, offset, current_line)
         return {'content': seg}
 
-    # Start parsing segments and parts.
+    # Start parsing segments.
     matches = list(segment_pattern.finditer(raw, offset))
     num_matches = len(matches)
     if num_matches > 0:
@@ -359,70 +351,41 @@
         first_offset = matches[0].start()
         if first_offset > 0:
             # There's some default content segment at the beginning.
-            seg = ContentSegment()
-            seg.parts, current_line = parse_segment_parts(
-                raw, offset, first_offset, current_line)
+            seg = ContentSegment(
+                raw[offset:first_offset], None, offset, current_line)
+            current_line += _count_lines(seg.content)
             contents['content'] = seg
 
         for i in range(1, num_matches):
             m1 = matches[i - 1]
             m2 = matches[i]
-            seg = ContentSegment()
-            seg.parts, current_line = parse_segment_parts(
-                raw, m1.end() + 1, m2.start(), current_line,
-                m1.group('fmt'))
+
+            cur_seg_start = m1.end() + 1
+            cur_seg_end = m2.start()
+
+            seg = ContentSegment(
+                raw[cur_seg_start:cur_seg_end],
+                m1.group('fmt'),
+                cur_seg_start,
+                current_line)
+            current_line += _count_lines(seg.content)
             contents[m1.group('name')] = seg
 
         # Handle text past the last match.
         lastm = matches[-1]
-        seg = ContentSegment()
-        seg.parts, current_line = parse_segment_parts(
-            raw, lastm.end() + 1, len(raw), current_line,
-            lastm.group('fmt'))
+
+        last_seg_start = lastm.end()
+
+        seg = ContentSegment(
+            raw[last_seg_start:],
+            lastm.group('fmt'),
+            last_seg_start,
+            current_line)
         contents[lastm.group('name')] = seg
+        # No need to count lines for the last one.
 
         return contents
     else:
         # No segments, just content.
-        seg = ContentSegment()
-        seg.parts, current_line = parse_segment_parts(
-            raw, offset, len(raw), current_line)
+        seg = ContentSegment(raw[offset:], None, offset, current_line)
         return {'content': seg}
-
-
-def parse_segment_parts(raw, start, end, line_offset, first_part_fmt=None):
-    matches = list(part_pattern.finditer(raw, start, end))
-    num_matches = len(matches)
-    if num_matches > 0:
-        parts = []
-
-        # First part, before the first format change.
-        part_text = raw[start:matches[0].start()]
-        parts.append(
-            ContentSegmentPart(part_text, first_part_fmt, start,
-                               line_offset))
-        line_offset += _count_lines(part_text)
-
-        for i in range(1, num_matches):
-            m1 = matches[i - 1]
-            m2 = matches[i]
-            part_text = raw[m1.end() + 1:m2.start()]
-            parts.append(
-                ContentSegmentPart(
-                    part_text, m1.group('fmt'), m1.end() + 1,
-                    line_offset))
-            line_offset += _count_lines(part_text)
-
-        lastm = matches[-1]
-        part_text = raw[lastm.end() + 1:end]
-        parts.append(ContentSegmentPart(
-            part_text, lastm.group('fmt'), lastm.end() + 1,
-            line_offset))
-
-        return parts, line_offset
-    else:
-        part_text = raw[start:end]
-        parts = [ContentSegmentPart(part_text, first_part_fmt, start,
-                                    line_offset)]
-        return parts, line_offset
-
--- a/piecrust/rendering.py	Fri Sep 29 19:59:19 2017 -0700
+++ b/piecrust/rendering.py	Sun Oct 01 20:36:04 2017 -0700
@@ -301,20 +301,17 @@
 
     formatted_segments = {}
     for seg_name, seg in page.segments.items():
-        seg_text = ''
-        for seg_part in seg.parts:
-            part_format = seg_part.fmt or format_name
-            try:
-                with app.env.stats.timerScope(
-                        engine.__class__.__name__ + '_segment'):
-                    part_text = engine.renderSegmentPart(
-                        page.content_spec, seg_part, page_data)
-            except TemplatingError as err:
-                err.lineno += seg_part.line
-                raise err
+        try:
+            with app.env.stats.timerScope(
+                    engine.__class__.__name__ + '_segment'):
+                seg_text = engine.renderSegment(
+                    page.content_spec, seg, page_data)
+        except TemplatingError as err:
+            err.lineno += seg.line
+            raise err
 
-            part_text = format_text(app, part_format, part_text)
-            seg_text += part_text
+        seg_format = seg.fmt or format_name
+        seg_text = format_text(app, seg_format, seg_text)
         formatted_segments[seg_name] = seg_text
 
         if seg_name == 'content':
--- a/piecrust/templating/base.py	Fri Sep 29 19:59:19 2017 -0700
+++ b/piecrust/templating/base.py	Sun Oct 01 20:36:04 2017 -0700
@@ -27,7 +27,7 @@
     def initialize(self, app):
         self.app = app
 
-    def renderSegmentPart(self, path, seg_part, data):
+    def renderSegment(self, path, segment, data):
         raise NotImplementedError()
 
     def renderFile(self, paths, data):
--- a/piecrust/templating/jinja/loader.py	Fri Sep 29 19:59:19 2017 -0700
+++ b/piecrust/templating/jinja/loader.py	Sun Oct 01 20:36:04 2017 -0700
@@ -5,11 +5,11 @@
 class PieCrustLoader(FileSystemLoader):
     def __init__(self, searchpath, encoding='utf-8'):
         super(PieCrustLoader, self).__init__(searchpath, encoding)
-        self.segment_parts_cache = {}
+        self.segments_cache = {}
 
     def get_source(self, environment, template):
-        if template.startswith('$part='):
-            filename, seg_part = self.segment_parts_cache[template]
+        if template.startswith('$seg='):
+            filename, seg_content = self.segments_cache[template]
 
             mtime = os.path.getmtime(filename)
 
@@ -19,6 +19,6 @@
                 except OSError:
                     return False
 
-            return seg_part, filename, uptodate
+            return seg_content, filename, uptodate
 
         return super(PieCrustLoader, self).get_source(environment, template)
--- a/piecrust/templating/jinjaengine.py	Fri Sep 29 19:59:19 2017 -0700
+++ b/piecrust/templating/jinjaengine.py	Sun Oct 01 20:36:04 2017 -0700
@@ -17,17 +17,17 @@
         self._jinja_syntax_error = None
         self._jinja_not_found = None
 
-    def renderSegmentPart(self, path, seg_part, data):
-        if not _string_needs_render(seg_part.content):
-            return seg_part.content
+    def renderSegment(self, path, segment, data):
+        if not _string_needs_render(segment.content):
+            return segment.content
 
         self._ensureLoaded()
 
-        part_path = _make_segment_part_path(path, seg_part.offset)
-        self.env.loader.segment_parts_cache[part_path] = (
-            path, seg_part.content)
+        seg_path = _make_segment_path(path, segment.offset)
+        self.env.loader.segments_cache[seg_path] = (
+            path, segment.content)
         try:
-            tpl = self.env.get_template(part_path)
+            tpl = self.env.get_template(seg_path)
         except self._jinja_syntax_error as tse:
             raise self._getTemplatingError(tse, filename=path)
         except self._jinja_not_found:
@@ -145,6 +145,6 @@
     return False
 
 
-def _make_segment_part_path(path, start):
-    return '$part=%s:%d' % (path, start)
+def _make_segment_path(path, start):
+    return '$seg=%s:%d' % (path, start)
 
--- a/piecrust/templating/pystacheengine.py	Fri Sep 29 19:59:19 2017 -0700
+++ b/piecrust/templating/pystacheengine.py	Sun Oct 01 20:36:04 2017 -0700
@@ -16,10 +16,10 @@
         self._not_found_error = None
         self._pystache_error = None
 
-    def renderSegmentPart(self, path, seg_part, data):
+    def renderSegment(self, path, segment, data):
         self._ensureLoaded()
         try:
-            return self.renderer.render(seg_part.content, data)
+            return self.renderer.render(segment.content, data)
         except self._not_found_error as ex:
             raise TemplateNotFoundError() from ex
         except self._pystache_error as ex: