diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 57c8d1ea..2e4d2a8d 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -649,19 +649,25 @@ class PersonalIniTab(QWidget): tooltip=_("Edit personal.ini"), use_find=True, save_size_name='ffdl:personal.ini') - error=False - while not error: - error=True + retry=True + while retry: d.exec_() if d.result() == d.Accepted: - self.personalini = unicode(d.get_plain_text()) - - errors = test_config(self.personalini) + editini = d.get_plain_text() + errors = test_config(editini) if errors: - error = not errors_dialog(self.plugin_action.gui, - _('Go back to fix errors?'), - '

'+'

'.join([ '%s %s'%e for e in errors ])+'

') + retry = errors_dialog(self.plugin_action.gui, + _('Go back to fix errors?'), + '

'+'

'.join([ '%s %s'%e for e in errors ])+'

') + else: + retry = False + + if not retry: + self.personalini = unicode(editini) + else: + # cancelled + retry = False class ReadingListTab(QWidget): diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index 142ffe65..b5226514 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -1260,8 +1260,6 @@ class IniTextDialog(SizePersistedDialog): button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) - # button_box.button(QDialogButtonBox.Ok).setDefault(False) - # button_box.button(QDialogButtonBox.Cancel).setDefault(False) self.l.addWidget(button_box) # Cause our dialog size to be restored from prefs or created on first usage @@ -1337,12 +1335,12 @@ def errors_dialog(parent, d = ViewLog(title,html,parent) return d.exec_() == d.Accepted - -class ViewLog(QDialog): +class ViewLog(SizePersistedDialog): - def __init__(self, title, html, parent=None): - QDialog.__init__(self, parent) + def __init__(self, title, html, parent=None, + save_size_name='ffdl:view log dialog',): + SizePersistedDialog.__init__(self, parent,save_size_name) self.l = l = QVBoxLayout() self.setLayout(l) @@ -1361,10 +1359,12 @@ class ViewLog(QDialog): # self.copy_button.clicked.connect(self.copy_to_clipboard) l.addWidget(self.bb) self.setModal(False) - self.resize(700, 500) self.setWindowTitle(title) self.setWindowIcon(QIcon(I('debug.png'))) #self.show() + + # Cause our dialog size to be restored from prefs or created on first usage + self.resize_dialog() def copy_to_clipboard(self): txt = self.tb.toPlainText() diff --git a/calibre-plugin/inihighlighter.py b/calibre-plugin/inihighlighter.py index b78c5744..8be745c2 100644 --- a/calibre-plugin/inihighlighter.py +++ b/calibre-plugin/inihighlighter.py @@ -30,6 +30,14 @@ class IniHighlighter(QSyntaxHighlighter): self.highlightingRules = [] + if entries: + # *known* entries + reentries = r'('+(r'|'.join(entries))+r')' + self.highlightingRules.append( HighlightingRule( r"\b"+reentries+r"\b", Qt.darkGreen ) ) + + # true/false -- just to be nice. + self.highlightingRules.append( HighlightingRule( r"\b(true|false)\b", Qt.darkGreen ) ) + # *all* keywords -- change known later. self.errorRule = HighlightingRule( r"^[^:=\s][^:=]*[:=]", Qt.red ) self.highlightingRules.append( self.errorRule ) @@ -38,14 +46,7 @@ class IniHighlighter(QSyntaxHighlighter): reentrykeywords = r'('+(r'|'.join([ e % r'[a-zA-Z0-9_]+' for e in entry_keywords ]))+r')' self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+reentrykeywords+r"\s*[:=]", Qt.darkMagenta ) ) - # true/false -- just to be nice. - self.highlightingRules.append( HighlightingRule( r"\b(true|false)\b", Qt.darkGreen ) ) - - if entries: - # *known* entries - reentries = r'('+(r'|'.join(entries))+r')' - self.highlightingRules.append( HighlightingRule( r"\b"+reentries+r"\b", Qt.darkGreen ) ) - + if entries: # separate from known entries so entry named keyword won't be masked. # *known* entry keywords reentrykeywords = r'('+(r'|'.join([ e % reentries for e in entry_keywords ]))+r')' self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+reentrykeywords+r"\s*[:=]", Qt.blue ) ) diff --git a/fanficdownloader/configurable.py b/fanficdownloader/configurable.py index 7dfe63ab..1aa48325 100644 --- a/fanficdownloader/configurable.py +++ b/fanficdownloader/configurable.py @@ -16,6 +16,7 @@ # import ConfigParser, re +import exceptions from ConfigParser import DEFAULTSECT, MissingSectionHeaderError, ParsingError # All of the writers(epub,html,txt) and adapters(ffnet,twlt,etc) @@ -58,157 +59,156 @@ def get_valid_sections(): return allowedsections def get_valid_list_entries(): - return list([ - 'category', - 'genre', - 'characters', - 'ships', - 'warnings', - 'extratags', - 'author', - 'authorId', - 'authorUrl', - 'lastupdate', - ]) + return list(['category', + 'genre', + 'characters', + 'ships', + 'warnings', + 'extratags', + 'author', + 'authorId', + 'authorUrl', + 'lastupdate', + ]) def get_valid_scalar_entries(): - return list([ - 'series', - 'seriesUrl', - 'language', - 'status', - 'datePublished', - 'dateUpdated', - 'dateCreated', - 'rating', - 'numChapters', - 'numWords', - 'site', - 'storyId', - 'title', - 'storyUrl', - 'description', - 'formatname', - 'formatext', - 'siteabbrev', - 'version', - # internal stuff. - 'authorHTML', - 'seriesHTML', - 'langcode', - 'output_css', - ]) + return list(['series', + 'seriesUrl', + 'language', + 'status', + 'datePublished', + 'dateUpdated', + 'dateCreated', + 'rating', + 'numChapters', + 'numWords', + 'site', + 'storyId', + 'title', + 'storyUrl', + 'description', + 'formatname', + 'formatext', + 'siteabbrev', + 'version', + # internal stuff. + 'authorHTML', + 'seriesHTML', + 'langcode', + 'output_css', + ]) def get_valid_entries(): return get_valid_list_entries() + get_valid_scalar_entries() +# *known* keywords -- or rather regexps for them. def get_valid_keywords(): - return list(['add_chapter_numbers', - 'add_genre_when_multi_category', - 'allow_unsafe_filename', - 'always_overwrite', - 'anthology_tags', - 'anthology_title_pattern', - 'background_color', - 'bulk_load', - 'chapter_end', - 'chapter_start', - 'chapter_title_add_pattern', - 'chapter_title_strip_pattern', - 'check_next_chapter', - 'collect_series', - 'connect_timeout', - 'convert_images_to', - 'cover_content', - 'cover_exclusion_regexp', - 'custom_columns_settings', - 'dateCreated_format', - 'datePublished_format', - 'dateUpdated_format', - 'default_cover_image', - 'do_update_hook', - 'exclude_notes', - 'extra_logpage_entries', - 'extra_subject_tags', - 'extra_titlepage_entries', - 'extra_valid_entries', - 'extratags', - 'extracategories', - 'extragenres', - 'extracharacters', - 'extraships', - 'extrawarnings', - 'fail_on_password', - 'file_end', - 'file_start', - 'fileformat', - 'find_chapters', - 'fix_fimf_blockquotes', - 'force_login', - 'generate_cover_settings', - 'grayscale_images', - 'image_max_size', - 'include_images', - 'include_logpage', - 'include_subject_tags', - 'include_titlepage', - 'include_tocpage', - '(in|ex)clude_metadata_(pre|post)', - 'is_adult', - 'join_string_authorHTML', - 'keep_style_attr', - 'keep_summary_html', - 'logpage_end', - 'logpage_entries', - 'logpage_entry', - 'logpage_start', - 'logpage_update_end', - 'logpage_update_start', - 'make_directories', - 'make_firstimage_cover', - 'make_linkhtml_entries', - 'max_fg_sleep', - 'max_fg_sleep_at_downloads', - 'min_fg_sleep', - 'never_make_cover', - 'no_image_processing', - 'non_breaking_spaces', - 'nook_img_fix', - 'output_css', - 'output_filename', - 'output_filename_safepattern', - 'password', - 'post_process_cmd', - 'remove_transparency', - 'replace_br_with_p', - 'replace_hr', - 'replace_metadata', - 'slow_down_sleep_time', - 'sort_ships', - 'strip_chapter_numbers', - 'strip_chapter_numeral', - 'strip_text_links', - 'titlepage_end', - 'titlepage_entries', - 'titlepage_entry', - 'titlepage_no_title_entry', - 'titlepage_start', - 'titlepage_use_table', - 'titlepage_wide_entry', - 'tocpage_end', - 'tocpage_entry', - 'tocpage_start', - 'tweak_fg_sleep', - 'universe_as_series', - 'user_agent', - 'username', - 'website_encodings', - 'wide_titlepage_entries', - 'windows_eol', - 'wrap_width', - 'zip_filename', - 'zip_output', - ]) + return list(['(in|ex)clude_metadata_(pre|post)', + 'add_chapter_numbers', + 'add_genre_when_multi_category', + 'allow_unsafe_filename', + 'always_overwrite', + 'anthology_tags', + 'anthology_title_pattern', + 'background_color', + 'bulk_load', + 'chapter_end', + 'chapter_start', + 'chapter_title_add_pattern', + 'chapter_title_strip_pattern', + 'check_next_chapter', + 'collect_series', + 'connect_timeout', + 'convert_images_to', + 'cover_content', + 'cover_exclusion_regexp', + 'custom_columns_settings', + 'dateCreated_format', + 'datePublished_format', + 'dateUpdated_format', + 'default_cover_image', + 'do_update_hook', + 'exclude_notes', + 'extra_logpage_entries', + 'extra_subject_tags', + 'extra_titlepage_entries', + 'extra_valid_entries', + 'extratags', + 'extracategories', + 'extragenres', + 'extracharacters', + 'extraships', + 'extrawarnings', + 'fail_on_password', + 'file_end', + 'file_start', + 'fileformat', + 'find_chapters', + 'fix_fimf_blockquotes', + 'force_login', + 'generate_cover_settings', + 'grayscale_images', + 'image_max_size', + 'include_images', + 'include_logpage', + 'include_subject_tags', + 'include_titlepage', + 'include_tocpage', + 'is_adult', + 'join_string_authorHTML', + 'keep_style_attr', + 'keep_summary_html', + 'logpage_end', + 'logpage_entries', + 'logpage_entry', + 'logpage_start', + 'logpage_update_end', + 'logpage_update_start', + 'make_directories', + 'make_firstimage_cover', + 'make_linkhtml_entries', + 'max_fg_sleep', + 'max_fg_sleep_at_downloads', + 'min_fg_sleep', + 'never_make_cover', + 'no_image_processing', + 'non_breaking_spaces', + 'nook_img_fix', + 'output_css', + 'output_filename', + 'output_filename_safepattern', + 'password', + 'post_process_cmd', + 'remove_transparency', + 'replace_br_with_p', + 'replace_hr', + 'replace_metadata', + 'slow_down_sleep_time', + 'sort_ships', + 'strip_chapter_numbers', + 'strip_chapter_numeral', + 'strip_text_links', + 'titlepage_end', + 'titlepage_entries', + 'titlepage_entry', + 'titlepage_no_title_entry', + 'titlepage_start', + 'titlepage_use_table', + 'titlepage_wide_entry', + 'tocpage_end', + 'tocpage_entry', + 'tocpage_start', + 'tweak_fg_sleep', + 'universe_as_series', + 'user_agent', + 'username', + 'website_encodings', + 'wide_titlepage_entries', + 'windows_eol', + 'wrap_width', + 'zip_filename', + 'zip_output', + ]) # *known* entry keywords -- or rather regexps for them. def get_valid_entry_keywords(): @@ -279,7 +279,7 @@ class Configuration(ConfigParser.SafeConfigParser): def getConfig(self, key, default=""): return self.get_config(self.sectionslist,key,default) - def get_config(self, sections, key, default=""): + def get_config(self, sections, key, default=""): val = default for section in sections: try: @@ -411,11 +411,50 @@ class Configuration(ConfigParser.SafeConfigParser): def test_config(self): errors=[] - + + teststory_re = re.compile(r'^teststory:(defaults|[0-9]+)$') allowedsections = get_valid_sections() + + clude_metadata_re = re.compile(r'(add_to_)?(in|ex)clude_metadata_(pre|post)') + + replace_metadata_re = re.compile(r'(add_to_)?replace_metadata') + from story import set_in_ex_clude, make_replacements + + custom_columns_settings_re = re.compile(r'(add_to_)?custom_columns_settings') + for section in self.sections(): - if section not in allowedsections and 'teststory:' not in section: + if section not in allowedsections and not teststory_re.match(section): errors.append((self.get_lineno(section),"Bad Section Name: %s"%section)) + else: + ## check each keyword in section. Due to precedence + ## order of sections, it's possible for bad lines to + ## never be used. + for keyword,value in self.items(section): + try: + + ## check regex bearing keywords first. Each + ## will raise exceptions if flawed. + if clude_metadata_re.match(keyword): + set_in_ex_clude(value) + + if replace_metadata_re.match(keyword): + make_replacements(value) + + # if custom_columns_settings_re.match(keyword): + #custom_columns_settings: + # cliches=>#acolumn + # themes=>#bcolumn,a + # timeline=>#ccolumn,n + # "FanFiction"=>#collection + + + ## skipping output_filename_safepattern + ## regex--not used with plugin and this isn't + ## used with CLI/web yet. + + except Exception as e: + errors.append((self.get_lineno(section,keyword),"Error:%s in (%s:%s)"%(e,keyword,value))) + return errors diff --git a/fanficdownloader/story.py b/fanficdownloader/story.py index d3bad278..a98938a3 100644 --- a/fanficdownloader/story.py +++ b/fanficdownloader/story.py @@ -288,6 +288,59 @@ class InExMatch: else: s='=' return u'InExMatch(%s %s%s %s)'%(self.keys,f,s,self.match) + +## metakey[,metakey]=~pattern +## metakey[,metakey]==string +## *for* part lines. Effect only when trailing conditional key=~regexp matches +## metakey[,metakey]=~pattern[&&metakey=~regexp] +## metakey[,metakey]==string[&&metakey=~regexp] +## metakey[,metakey]=~pattern[&&metakey==string] +## metakey[,metakey]==string[&&metakey==string] +def set_in_ex_clude(setting): + dest = [] + # print("set_in_ex_clude:"+setting) + for line in setting.splitlines(): + if line: + (match,condmatch)=(None,None) + if "&&" in line: + (line,conditional) = line.split("&&") + condmatch = InExMatch(conditional) + match = InExMatch(line) + dest.append([match,condmatch]) + return dest + +## Two or three part lines. Two part effect everything. +## Three part effect only those key(s) lists. +## pattern=>replacement +## metakey,metakey=>pattern=>replacement +## *Five* part lines. Effect only when trailing conditional key=>regexp matches +## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp] +def make_replacements(replace): + retval=[] + for line in replace.splitlines(): + # print("replacement line:%s"%line) + (metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None) + if "&&" in line: + (line,conditional) = line.split("&&") + (condkey,condregexp) = conditional.split("=>") + if "=>" in line: + parts = line.split("=>") + if len(parts) > 2: + metakeys = map( lambda x: x.strip(), parts[0].split(",") ) + (regexp,replacement)=parts[1:] + else: + (regexp,replacement)=parts + + if regexp: + regexp = re_compile(regexp,line) + if condregexp: + condregexp = re_compile(condregexp,line) + # A way to explicitly include spaces in the + # replacement string. The .ini parser eats any + # trailing spaces. + replacement=replacement.replace(SPACE_REPLACE,' ') + retval.append([metakeys,regexp,replacement,condkey,condregexp]) + return retval class Story(Configurable): @@ -298,7 +351,6 @@ class Story(Configurable): self.metadata = {'version':os.environ['CURRENT_VERSION_ID']} except: self.metadata = {'version':'4.4'} - self.replacements = [] self.in_ex_cludes = {} self.chapters = [] # chapters will be tuples of (title,html) self.imgurls = [] @@ -318,7 +370,7 @@ class Story(Configurable): for val in self.getConfigList(config): self.addToList(metadata,val) - self.setReplace(self.getConfig('replace_metadata')) + self.replacements = make_replacements(self.getConfig('replace_metadata')) in_ex_clude_list = ['include_metadata_pre','exclude_metadata_pre', 'include_metadata_post','exclude_metadata_post'] @@ -327,7 +379,7 @@ class Story(Configurable): # print("%s %s"%(ie,ies)) if ies: iel = [] - self.in_ex_cludes[ie] = self.set_in_ex_clude(ies) + self.in_ex_cludes[ie] = set_in_ex_clude(ies) def join_list(self, key, vallist): return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist)) @@ -357,26 +409,6 @@ class Story(Configurable): self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d")) - ## metakey[,metakey]=~pattern - ## metakey[,metakey]==string - ## *for* part lines. Effect only when trailing conditional key=~regexp matches - ## metakey[,metakey]=~pattern[&&metakey=~regexp] - ## metakey[,metakey]==string[&&metakey=~regexp] - ## metakey[,metakey]=~pattern[&&metakey==string] - ## metakey[,metakey]==string[&&metakey==string] - def set_in_ex_clude(self,setting): - dest = [] - # print("set_in_ex_clude:"+setting) - for line in setting.splitlines(): - if line: - (match,condmatch)=(None,None) - if "&&" in line: - (line,conditional) = line.split("&&") - condmatch = InExMatch(conditional) - match = InExMatch(line) - dest.append([match,condmatch]) - return dest - def do_in_ex_clude(self,which,value,key): if value and which in self.in_ex_cludes: include = 'include' in which @@ -407,37 +439,6 @@ class Story(Configurable): return value - ## Two or three part lines. Two part effect everything. - ## Three part effect only those key(s) lists. - ## pattern=>replacement - ## metakey,metakey=>pattern=>replacement - ## *Five* part lines. Effect only when trailing conditional key=>regexp matches - ## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp] - def setReplace(self,replace): - for line in replace.splitlines(): - # print("replacement line:%s"%line) - (metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None) - if "&&" in line: - (line,conditional) = line.split("&&") - (condkey,condregexp) = conditional.split("=>") - if "=>" in line: - parts = line.split("=>") - if len(parts) > 2: - metakeys = map( lambda x: x.strip(), parts[0].split(",") ) - (regexp,replacement)=parts[1:] - else: - (regexp,replacement)=parts - - if regexp: - regexp = re_compile(regexp,line) - if condregexp: - condregexp = re_compile(condregexp,line) - # A way to explicitly include spaces in the - # replacement string. The .ini parser eats any - # trailing spaces. - replacement=replacement.replace(SPACE_REPLACE,' ') - self.replacements.append([metakeys,regexp,replacement,condkey,condregexp]) - def doReplacements(self,value,key,return_list=False,seen_list=[]): value = self.do_in_ex_clude('include_metadata_pre',value,key) value = self.do_in_ex_clude('exclude_metadata_pre',value,key)