Remove theme parser
authorJack Miller <millerjo@us.ibm.com>
Wed, 3 Jun 2015 21:23:32 +0000 (16:23 -0500)
committerJack Miller <millerjo@us.ibm.com>
Wed, 3 Jun 2015 22:08:44 +0000 (17:08 -0500)
This is far better done in Python with plugins instead of a clusterfuck
of escapes and shite.

canto_curses/config.py
canto_curses/guibase.py
canto_curses/parser.py [deleted file]
canto_curses/reader.py
canto_curses/story.py
canto_curses/tag.py
canto_curses/tagcore.py
canto_curses/theme.py

index fe4bfe6..e79b1c0 100644 (file)
@@ -21,10 +21,6 @@ from canto_next.hooks import call_hook
 from canto_next.rwlock import RWLock, write_lock, read_lock
 from canto_next.remote import assign_to_dict, access_dict
 
-DEFAULT_FSTRING = "%?{sel}(%{selected}:%{unselected})%?{m}(%{marked}:%{unmarked})%?{rd}(%{read}:%{unread})%{pre}%t%{post}%?{m}(%{marked_end}:%{unmarked_end})%?{rd}(%{read_end}:%{unread_end})%?{sel}(%{selected_end}:%{unselected_end})"
-
-DEFAULT_TAG_FSTRING = "%?{sel}(%{selected}:%{unselected})%?{c}([+]:[-])%{pre} %t %{post} [%B%1%n%0%b]% %?{pending}([%8%B%{pending}%b%0]:)%?{sel}(%{selected_end}:%{unselected_end})"
-
 from .locks import config_lock
 from .subthread import SubThread
 
@@ -70,6 +66,8 @@ def needs_eval(option):
             return True
     return False
 
+story_needed_attrs = [ "title" ]
+
 class CantoCursesConfig(SubThread):
 
     # The object init just sets up the default settings, doesn't
@@ -113,15 +111,6 @@ class CantoCursesConfig(SubThread):
             "tags" : self.validate_tags,
             "tagorder" : self.validate_tag_order,
 
-            "tagobj" :
-            {
-                "format" : self.validate_string,
-                "selected" : self.validate_string,
-                "unselected" : self.validate_string,
-                "selected_end" : self.validate_string,
-                "unselected_end" : self.validate_string,
-            },
-
             "update" :
             {
                 "style" : self.validate_update_style,
@@ -156,21 +145,6 @@ class CantoCursesConfig(SubThread):
             "story" :
             {
                 "enumerated" : self.validate_bool,
-                "format" : self.validate_string,
-                "format_attrs" : self.validate_string_list,
-
-                "selected": self.validate_string,
-                "unselected":  self.validate_string,
-                "selected_end": self.validate_string ,
-                "unselected_end": self.validate_string,
-                "read": self.validate_string,
-                "unread": self.validate_string,
-                "read_end": self.validate_string,
-                "unread_end": self.validate_string,
-                "marked": self.validate_string,
-                "unmarked": self.validate_string,
-                "marked_end": self.validate_string,
-                "unmarked_end": self.validate_string,
             },
 
             "input" : { "window" : self.validate_window },
@@ -214,15 +188,6 @@ class CantoCursesConfig(SubThread):
             "tags" : r"maintag:.*",
             "tagorder" : [],
 
-            "tagobj" :
-            {
-                "format" : DEFAULT_TAG_FSTRING,
-                "selected" : "%R",
-                "unselected" : "",
-                "selected_end" : "%r",
-                "unselected_end" : "",
-            },
-
             "update" :
             {
                 "style" : "append",
@@ -324,22 +289,6 @@ class CantoCursesConfig(SubThread):
             "story" :
             {
                 "enumerated" : False,
-                "format" : DEFAULT_FSTRING,
-                "format_attrs" : [ "title" ],
-
-                # Themability
-                "selected": "%R",
-                "unselected": "",
-                "selected_end": "%r",
-                "unselected_end": "",
-                "read": "%2",
-                "unread": "%1%B",
-                "read_end": "%0",
-                "unread_end": "%b%0",
-                "marked": "%B[*] ",
-                "unmarked": "",
-                "marked_end": "%b",
-                "unmarked_end": "",
             },
 
             "input" :
index 72f6c6c..6bd97a8 100644 (file)
@@ -12,7 +12,7 @@ from canto_next.remote import assign_to_dict, access_dict
 
 from .command import CommandHandler, register_commands, register_arg_types, unregister_all, _string, register_aliases, commands, command_help
 from .tagcore import tag_updater
-from .parser import prep_for_display
+from .theme import prep_for_display
 from .config import needs_eval, config
 
 import logging
diff --git a/canto_curses/parser.py b/canto_curses/parser.py
deleted file mode 100644 (file)
index 977c157..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-# -*- coding: utf-8 -*-
-#Canto-curses - ncurses RSS reader
-#   Copyright (C) 2014 Jack Miller <jack@codezen.org>
-#
-#   This program is free software; you can redistribute it and/or modify
-#   it under the terms of the GNU General Public License version 2 as 
-#   published by the Free Software Foundation.
-
-# This code provides two top level functions:
-# parse_conditionals - which will return a list of strings and dicts that are
-#    effectively a parse tree for all of the conditionals in the given format
-#    string
-#
-# eval_theme_string - which will return the final, formatted string given their 
-#    associated values.
-
-# NOTE: This code guarantees that any given conditional expression or other
-# eval()'d string will be evaluated exactly once. This means that it's
-# impossible to cause the code to infinitely recurse with a value like
-# { 'a' : '%a' }.
-
-from .html import html_entity_convert, char_ref_convert
-
-import traceback
-import logging
-import re
-
-log = logging.getLogger("PARSER")
-
-# Break the first conditional out of a string
-# For example two top level ternaries: 
-#   "Prefix %?{a}(true : false) %?{b}(trueb : falseb)"
-# Will return
-#   ['Prefix', { 'a' : { True : ["true"], False : ["false"] }}, ' %?{b}(trueb : falseb)']
-# ( Note the second b ternary hasn't been split out )
-
-tern_start = re.compile("(.*?[^\\\\]?)%\\?{([^}]*)}\\(")
-
-def _parse_one_conditional(uni):
-    strings = []
-
-    m = tern_start.match(uni)
-
-    # No ternaries detected.
-    if not m:
-        return [ uni ]
-
-    # Append the potential prefix.
-    strings.append(m.group(1))
-
-    code = m.group(2)
-    # Add the relevant escape
-    strings.append({ code : {}})
-
-    escaped = False
-    paren = 1
-    value = ""
-
-    for i, c in enumerate(uni[m.end():]):
-        if escaped:
-            value += c
-            escaped = False
-        elif c == "\\":
-            escaped = True
-        elif c == "(":
-            paren += 1
-            value += c
-        elif c == ":" and paren == 1:
-            strings[-1][code][True] = value
-            value = ""
-        elif c == ")":
-            paren -= 1
-
-            # This is the closing paren
-            if paren == 0:
-                strings[-1][code][False] = value
-
-                # Append the rest of the string.
-                value = uni[i + m.end() + 1:]
-                if value:
-                    strings.append(value)
-                    break
-
-            # Not the right paren, include it.
-            else:
-                value += c
-
-        # Normal character, include it.
-        else:
-            value += c
-
-    return strings
-
-# Like the above, except recurse to find the final
-# representation of the string.
-
-def parse_conditionals(uni):
-    strings = _parse_one_conditional(uni)
-
-    # If there were no conditionals,
-    # no need to continue.
-    if len(strings) == 1:
-        return strings
-
-    # Otherwise, check the resulting strings
-    # for ternaries.
-
-    ret_strings = []
-
-    for term in strings:
-        if type(term) == dict:
-
-            # For now, toplevel dicts will have only one
-            # key, and I can't see a reason to expand it
-            # but let's iterate anyway.
-
-            for topkey in term:
-                for subkey in term[topkey]:
-                    term[topkey][subkey] =\
-                            parse_conditionals(term[topkey][subkey])
-            ret_strings.append(term)
-        else:
-            ret_strings += parse_conditionals(term)
-
-    return ret_strings
-
-# This function evaluates a simple string, detecting any
-# python eval sequences.
-
-def _eval_simple(uni, values):
-    r = ""
-    escaped = False
-
-    in_code = False
-    long_code = False
-    code = ""
-
-    for c in uni:
-        if escaped:
-            if in_code:
-                code += c
-            else:
-                r += c
-            escaped = False
-        elif c == '\\':
-            escaped = True
-
-        elif c == '}' and in_code and long_code:
-            r += str(eval(code, {}, values))
-            code = ""
-            in_code = False
-            long_code = False
-        elif c == '{' and in_code and code == "":
-            long_code = True
-        elif in_code:
-            if long_code:
-                code += c
-            elif c in values:
-                r += str(values[c])
-                in_code = False
-            else:
-                Exception("Unknown escape: %s" % c)
-                in_code = False
-        elif c == '%':
-            in_code = True
-        else:
-            r += c
-
-    return r
-
-def eval_theme_string(parsed, values):
-    r = ""
-    for term in parsed:
-        if type(term) == dict:
-
-            # Once again, iterate even though the top level dicts only have
-            # a single key. This will cause trouble if there are multiple
-            # keys however, as the order is varied.
-
-            for topkey in term:
-                val = eval(topkey, {}, values)
-                if val:
-                    r += eval_theme_string(term[topkey][True], values)
-                else:
-                    r += eval_theme_string(term[topkey][False], values)
-        else:
-            r += _eval_simple(term, values)
-    return r
-
-def prep_for_display(s):
-    s = s.replace("\\", "\\\\")
-    s = s.replace("%", "\\%")
-    s = html_entity_convert(s)
-    s = char_ref_convert(s)
-    return s
-
-def try_parse(s, default):
-    try:
-        parsed = parse_conditionals(s)
-    except Exception as e:
-        log.warn("Failed to parse conditionals in fstring: %s" % s)
-        log.warn("\n" + "".join(traceback.format_exc()))
-        log.warn("Falling back to default.")
-        parsed = parse_conditionals(default)
-    return parsed
-
-def try_eval(parsed, values, fallback_parse):
-    try:
-        s = eval_theme_string(parsed, values)
-    except Exception as e:
-        log.warn("Failed to evaluate fstring: %s with %s" % (parsed, values))
-        log.warn("\n" + "".join(traceback.format_exc()))
-        log.warn("Falling back to default")
-
-        parsed = parse_conditionals(fallback_parse)
-        s = eval_theme_string(parsed, values)
-    return s
index 41dbe60..bd13d8c 100644 (file)
@@ -10,7 +10,7 @@ from canto_next.plugins import Plugin
 from canto_next.hooks import on_hook, remove_hook, unhook_all
 
 from .command import register_commands, register_arg_types, unregister_all, _int_range
-from .parser import prep_for_display
+from .theme import prep_for_display
 from .html import htmlparser
 from .text import TextBox
 from .tagcore import tag_updater
index 6a1188e..0ce13c3 100644 (file)
@@ -9,10 +9,9 @@
 from canto_next.plugins import Plugin, PluginHandler
 from canto_next.hooks import on_hook, unhook_all
 
-from .theme import FakePad, WrapPad, theme_print, theme_len, theme_reset, theme_border
-from .parser import try_parse, try_eval, prep_for_display
-from .config import DEFAULT_FSTRING
+from .theme import FakePad, WrapPad, theme_print, theme_len, theme_reset, theme_border, prep_for_display
 from .tagcore import tag_updater
+from .config import story_needed_attrs
 
 import traceback
 import logging
@@ -252,6 +251,35 @@ class Story(PluginHandler):
         self.changed = True
         self.callbacks["set_var"]("needs_refresh", True)
 
+    def eval(self):
+        s = ""
+
+        if self.selected:
+            s += "%R"
+
+        if self.marked:
+            s += "%B[*]"
+
+        if "read" in self.content["canto-state"]:
+            s += "%2"
+        else:
+            s += "%1%B"
+
+        s += prep_for_display(self.content["title"])
+
+        if "read" in self.content["canto-state"]:
+            s += "%0"
+        else:
+            s += "%b%0"
+
+        if self.marked:
+            s += "%b"
+
+        if self.selected:
+            s += "%r"
+
+        return s
+
     def lines(self, width):
         if width == self.width and not self.changed:
             return self.lns + self.extra_lines
@@ -259,12 +287,10 @@ class Story(PluginHandler):
         # Make sure we actually have all of the attributes needed
         # to complete the render.
 
-        story_conf = self.callbacks["get_opt"]("story")
-
-        self.enumerated = story_conf["enumerated"]
+        self.enumerated = self.callbacks["get_opt"]("story.enumerated")
         self.rel_enumerated = self.callbacks["get_tag_opt"]("enumerated")
 
-        for attr in story_conf["format_attrs"]:
+        for attr in story_needed_attrs:
             if attr not in self.content:
 
                 # Not having needed info is a good reason to
@@ -285,46 +311,7 @@ class Story(PluginHandler):
                 self.lns = 1
                 return self.lns
 
-        parsed = try_parse(story_conf["format"], DEFAULT_FSTRING)
-        parsed_pre = try_parse(self.pre_format, "")
-        parsed_post = try_parse(self.post_format, "")
-
-        # These are escapes that are handled in the theme_print
-        # lower in the function and should remain present after
-        # evaluation.
-
-        passthru = {}
-        for c in "RrDdUuBbSs012345678[":
-            passthru[c] = "%" + c
-
-        # Add refactored themability variables:
-
-        for attr in [ "selected", "read", "marked" ]:
-            passthru[attr] = story_conf[attr]
-            passthru["un" + attr] = story_conf["un" + attr]
-            passthru[attr + "_end"] = story_conf[attr + "_end"]
-            passthru["un" + attr + "_end"] = story_conf["un" + attr + "_end"]
-
-        values = { 'sel' : self.selected,
-                    'm' : self.marked,
-                   'rd' : "read" in self.content["canto-state"],
-                   'ut' : self.content["canto-tags"],
-                    't' : self.content["title"],
-                    'l' : self.content["link"],
-                 'item' : self,
-                 'prep' : prep_for_display}
-
-        # Prep all text values for display.
-
-        for value in list(values.keys()):
-            if type(values[value]) == str:
-                values[value] = prep_for_display(values[value])
-
-        values.update(passthru)
-
-        values["pre"] = try_eval(parsed_pre, values, "")
-        values["post"] = try_eval(parsed_post, values, "")
-        self.evald_string = try_eval(parsed, values, DEFAULT_FSTRING)
+        self.evald_string = self.eval()
 
         taglist_conf = self.callbacks["get_opt"]("taglist")
 
index a636d08..f5b6fd5 100644 (file)
@@ -13,7 +13,7 @@ from canto_next.rwlock import read_lock
 from .locks import sync_lock, config_lock
 from .parser import try_parse, try_eval, prep_for_display
 from .theme import FakePad, WrapPad, theme_print, theme_reset, theme_border
-from .config import config, DEFAULT_TAG_FSTRING
+from .config import config
 from .story import Story
 
 import traceback
@@ -233,18 +233,7 @@ class Tag(PluginHandler, list):
         self.changed = True
         self.callbacks["set_var"]("needs_redraw", True)
 
-    def lines(self, width):
-        if width == self.width and not self.changed:
-            return self.lns
-
-        tag_conf = self.callbacks["get_opt"]("tagobj")
-        taglist_conf = self.callbacks["get_opt"]("taglist")
-
-        # Values to pass on to render
-        self.collapsed = self.callbacks["get_tag_opt"]("collapsed")
-        self.border = taglist_conf["border"]
-        self.enumerated = taglist_conf["tags_enumerated"]
-        self.abs_enumerated = taglist_conf["tags_enumerated_absolute"]
+    def eval(self):
 
         # Make sure to strip out the category from category:name
         tag = self.tag.split(':', 1)[1]
@@ -253,52 +242,46 @@ class Tag(PluginHandler, list):
                 if "canto-state" not in s.content or\
                 "read" not in s.content["canto-state"]])
 
-        extra_tags = self.callbacks["get_tag_conf"](self.tag)['extra_tags']
-
-        # These are escapes that are handled in the theme_print
-        # lower in the function and should remain present after
-        # evaluation.
+        s = ""
+        if self.selected:
+            s += "%R"
 
-        passthru = {}
-        for c in "RrDdUuBbSs012345678[":
-            passthru[c] = "%" + c
+        if self.collapsed:
+            s += "[+]"
+        else:
+            s += "[-]"
 
-        for attr in [ "selected", "unselected", "selected_end", "unselected_end" ]:
-            passthru[attr] = tag_conf[attr]
+        s += " " + tag + " "
 
-        passthru['pre'] = self.pre_format
-        passthru['post'] = self.post_format
+        s += "[%B%1" + str(unread) + "%0%b]"
+        if self.updates_pending:
+            s += " [%8%B" + str(self.updates_pending) + "%b%0]"
 
-        parsed = try_parse(tag_conf["format"], DEFAULT_TAG_FSTRING)
-        parsed_pre = try_parse(self.pre_format, "")
-        parsed_post = try_parse(self.post_format, "")
+        if self.selected:
+            s += "%r"
 
-        values = {  'c' : self.collapsed,
-                    't' : tag,
-                    'sel' : self.selected,
-                    'n' : unread,
-                    "extra_tags" : extra_tags,
-                    'tag' : self,
-                    'pending' : self.updates_pending,
-                    'prep' : prep_for_display}
+        return s
 
-        # Prep all text values for display.
+    def lines(self, width):
+        if width == self.width and not self.changed:
+            return self.lns
 
-        for value in list(values.keys()):
-            if type(values[value]) in [str, str]:
-                values[value] = prep_for_display(values[value])
+        taglist_conf = self.callbacks["get_opt"]("taglist")
 
-        values.update(passthru)
+        self.collapsed = self.callbacks["get_tag_opt"]("collapsed")
+        self.border = taglist_conf["border"]
+        self.enumerated = taglist_conf["tags_enumerated"]
+        self.abs_enumerated = taglist_conf["tags_enumerated_absolute"]
 
-        values["pre"] = try_eval(parsed_pre, values, "")
-        values["post"] = try_eval(parsed_post, values, "")
-        self.evald_string = try_eval(parsed, values, DEFAULT_TAG_FSTRING)
+        extra_tags = self.callbacks["get_tag_conf"](self.tag)['extra_tags']
 
         self.pad = None
         self.footpad = None
         self.width = width
         self.changed = False
 
+        self.evald_string = self.eval()
+
         self.lns = self.render_header(width, FakePad(width))
         self.footlines = self.render_footer(width, FakePad(width))
 
index 8ec6ead..e325d33 100644 (file)
@@ -11,7 +11,7 @@ from canto_next.hooks import call_hook, on_hook
 
 from .subthread import SubThread
 from .locks import config_lock
-from .config import config
+from .config import config, story_needed_attrs
 
 import traceback
 import logging
@@ -116,13 +116,9 @@ class TagUpdater(SubThread):
 
         self.needed_attrs = [ "title", "canto-state", "canto-tags", "link", "enclosures" ]
 
-        # Make sure we grab attributes needed for the story
-        # format and story format.
-
-        sfa = config.get_opt("story.format_attrs")
         tsa = config.get_opt("taglist.search_attributes")
 
-        for attrlist in [ sfa, tsa ]:
+        for attrlist in [ story_needed_attrs, tsa ]:
             for sa in attrlist:
                 if sa not in self.needed_attrs:
                     self.needed_attrs.append(sa)
index 5356508..e262d3f 100644 (file)
@@ -8,6 +8,7 @@
 
 from canto_next.encoding import encoder, locale_enc
 from .widecurse import waddch, wcwidth
+from .html import html_entity_convert, char_ref_convert
 
 import curses
 
@@ -397,3 +398,10 @@ def theme_border(code):
     if "UTF-8" in locale_enc:
         return utf_chars[code]
     return ascii_chars[code]
+
+def prep_for_display(s):
+    s = s.replace("\\", "\\\\")
+    s = s.replace("%", "\\%")
+    s = html_entity_convert(s)
+    s = char_ref_convert(s)
+    return s