From 2358cc247fd08e11d668137690c1e91707e4305a Mon Sep 17 00:00:00 2001 From: "St.Huang" Date: Sat, 9 Feb 2019 12:06:48 +0800 Subject: [PATCH 1/6] fix #120 --- addons21/fastwq/libs/mdict/mdict_query.py | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/addons21/fastwq/libs/mdict/mdict_query.py b/addons21/fastwq/libs/mdict/mdict_query.py index 0ac9119..2ec0442 100644 --- a/addons21/fastwq/libs/mdict/mdict_query.py +++ b/addons21/fastwq/libs/mdict/mdict_query.py @@ -1,17 +1,19 @@ # -*- coding: utf-8 -*- - -from .readmdict import MDX, MDD -from struct import pack, unpack -from io import BytesIO -import re -import sys -import os -import sqlite3 import json - +import os +import re +import sqlite3 +import sys # zlib compression is used for engine version >=2.0 import zlib +from io import BytesIO +from struct import pack, unpack + +import chardet + +from .readmdict import MDD, MDX + # LZO compression is used for engine version < 2.0 try: import lzo @@ -110,16 +112,22 @@ class IndexBuilder(object): def _replace_stylesheet(self, txt): # substitute stylesheet definition + encoding = 'utf-8' + if isinstance(txt, bytes): + encode_type = chardet.detect(txt) + encoding = encode_type['encoding'] + txt = txt.decode(encoding) txt_list = re.split('`\d+`', txt) txt_tag = re.findall('`\d+`', txt) txt_styled = txt_list[0] for j, p in enumerate(txt_list[1:]): style = self._stylesheet[txt_tag[j][1:-1]] if p and p[-1] == '\n': - txt_styled = txt_styled + style[0] + p.rstrip() + style[1] + '\r\n' + txt_styled = txt_styled + style[0] + p.rstrip( + ) + style[1] + '\r\n' else: txt_styled = txt_styled + style[0] + p + style[1] - return txt_styled + return txt_styled.encode(encoding) def _make_mdx_index(self, db_name): if os.path.exists(db_name): From a3f3d8eb957f564e699da427c00d5521be42f639 Mon Sep 17 00:00:00 2001 From: "St.Huang" Date: Sat, 9 Feb 2019 19:27:43 +0800 Subject: [PATCH 2/6] add a reset button in the settings dialog --- addons21/fastwq/context.py | 22 +- addons21/fastwq/gui/common.py | 38 +- addons21/fastwq/gui/setting.py | 44 +- addons21/fastwq/libs/AnkiHub/__init__.py | 0 addons21/fastwq/libs/AnkiHub/markdown2.py | 2584 --------------------- addons21/fastwq/libs/AnkiHub/updates.py | 48 - addons21/fastwq/libs/ankihub.py | 311 --- 7 files changed, 50 insertions(+), 2997 deletions(-) delete mode 100644 addons21/fastwq/libs/AnkiHub/__init__.py delete mode 100644 addons21/fastwq/libs/AnkiHub/markdown2.py delete mode 100644 addons21/fastwq/libs/AnkiHub/updates.py delete mode 100644 addons21/fastwq/libs/ankihub.py diff --git a/addons21/fastwq/context.py b/addons21/fastwq/context.py index 85a0800..aed659b 100644 --- a/addons21/fastwq/context.py +++ b/addons21/fastwq/context.py @@ -19,16 +19,17 @@ import json import os -from aqt import mw + from anki.hooks import runHook +from aqt import mw + from .constants import VERSION from .utils import get_icon - __all__ = ['APP_ICON', 'config'] -APP_ICON = get_icon('wqicon.png') #Addon Icon +APP_ICON = get_icon('wqicon.png') # Addon Icon class Config(object): @@ -37,7 +38,7 @@ class Config(object): Addon Config """ - _CONFIG_FILENAME = 'fastwqcfg.json' #Config File Path + _CONFIG_FILENAME = 'fastwqcfg.json' # Config File Path def __init__(self, window): self.path = u'_' + self._CONFIG_FILENAME @@ -122,7 +123,7 @@ class Config(object): @property def last_folder(self): """ - last file dialog open path + last file dialog open path """ return self.data.get('last_folder', '') @@ -131,15 +132,12 @@ class Config(object): '''ignore accents of field in querying''' return self.data.get('ignore_accents', False) - @property - def auto_update(self): - '''auto check new version''' - return self.data.get('auto_update', True) - @property def cloze_str(self): '''cloze formater string''' - return self.data.get('cloze_str', '{{c1::%s}}') - + tmpstr = self.data.get('cloze_str', '{{c1::%s}}') + if len(tmpstr.split('%s')) != 2: + tmpstr = '{{c1::%s}}' + return tmpstr config = Config(mw) diff --git a/addons21/fastwq/gui/common.py b/addons21/fastwq/gui/common.py index eb7c37c..3a5d444 100644 --- a/addons21/fastwq/gui/common.py +++ b/addons21/fastwq/gui/common.py @@ -18,38 +18,22 @@ # along with this program. If not, see . import types + from aqt import mw from aqt.qt import * -from aqt.utils import showInfo -from .options import OptionsDialog -from .foldermanager import FoldersManageDialog -from .dictmanager import DictManageDialog -from ..libs import ankihub + +from ..constants import Template from ..context import config from ..lang import _ -from ..constants import Endpoint, Template from ..service import service_manager, service_pool +from .dictmanager import DictManageDialog +from .foldermanager import FoldersManageDialog +from .options import OptionsDialog + +__all__ = ['show_options', 'show_fm_dialog', 'show_about_dialog'] -__all__ = ['show_options', 'show_fm_dialog', 'show_about_dialog'] #'check_updates', - - -# def check_updates(background=False, parent=None): -# '''check add-on last version''' -# try: -# parent = mw if parent is None else parent -# state = ankihub.update([Endpoint.check_version], Endpoint.version, background, parent) -# if not background: -# if state == 0: -# showInfo(_('LATEST_VERSION')) -# elif state == -1: -# showInfo(_('CHECK_FAILURE')) -# except: -# if not background: -# showInfo(_('CHECK_FAILURE')) - - -def show_fm_dialog(browser = None): +def show_fm_dialog(browser=None): '''open dictionary folder manager window''' parent = mw if browser is None else browser fm_dialog = FoldersManageDialog(parent, u'Dictionary Folder Manager') @@ -64,7 +48,7 @@ def show_fm_dialog(browser = None): show_options(browser) -def show_dm_dialog(browser = None): +def show_dm_dialog(browser=None): parent = mw if browser is None else browser dm_dialog = DictManageDialog(parent, u'Dictionary Manager') dm_dialog.activateWindow() @@ -78,7 +62,7 @@ def show_dm_dialog(browser = None): show_options(browser) -def show_options(browser = None, model_id = -1, callback = None, *args, **kwargs): +def show_options(browser=None, model_id=-1, callback=None, *args, **kwargs): '''open options window''' parent = mw if browser is None else browser config.read() diff --git a/addons21/fastwq/gui/setting.py b/addons21/fastwq/gui/setting.py index 051c3d1..842454d 100644 --- a/addons21/fastwq/gui/setting.py +++ b/addons21/fastwq/gui/setting.py @@ -18,10 +18,10 @@ # along with this program. If not, see . from aqt.qt import * -from .base import Dialog, WIDGET_SIZE + from ..context import config from ..lang import _ - +from .base import WIDGET_SIZE, Dialog __all__ = ['SettingDialog'] @@ -36,7 +36,6 @@ class SettingDialog(Dialog): self.setFixedWidth(400) self.check_force_update = None self.check_ignore_accents = None - # self.check_auto_update = None self.input_thread_number = None self.build() @@ -53,11 +52,6 @@ class SettingDialog(Dialog): layout.addWidget(check_ignore_accents) layout.addSpacing(10) - # check_auto_update = QCheckBox(_("AUTO_UPDATE")) - # check_auto_update.setChecked(config.auto_update) - # layout.addWidget(check_auto_update) - # layout.addSpacing(10) - check_ighore_mdx_wordcase = QCheckBox(_("IGNORE_MDX_WORDCASE")) check_ighore_mdx_wordcase.setChecked(config.ignore_mdx_wordcase) layout.addWidget(check_ighore_mdx_wordcase) @@ -84,16 +78,23 @@ class SettingDialog(Dialog): hbox.setStretchFactor(input_cloze_str, 2) layout.addLayout(hbox) - buttonBox = QDialogButtonBox(parent=self) - buttonBox.setStandardButtons(QDialogButtonBox.Ok) - buttonBox.accepted.connect(self.accept) # 确定 + hbox = QHBoxLayout() + okbtn = QDialogButtonBox(parent=self) + okbtn.setStandardButtons(QDialogButtonBox.Ok) + okbtn.clicked.connect(self.accept) + resetbtn = QDialogButtonBox(parent=self) + resetbtn.setStandardButtons(QDialogButtonBox.Reset) + resetbtn.clicked.connect(self.reset) + hbox.setAlignment(Qt.AlignRight) + hbox.addSpacing(300) + hbox.addWidget(resetbtn) + hbox.addWidget(okbtn) layout.addSpacing(48) - layout.addWidget(buttonBox) + layout.addLayout(hbox) self.check_force_update = check_force_update self.check_ignore_accents = check_ignore_accents - # self.check_auto_update = check_auto_update self.check_ighore_mdx_wordcase = check_ighore_mdx_wordcase self.input_thread_number = input_thread_number self.input_cloze_str = input_cloze_str @@ -105,14 +106,27 @@ class SettingDialog(Dialog): self.save() super(SettingDialog, self).accept() + def reset(self): + data = { + 'force_update': False, + 'ignore_accents': False, + 'ignore_mdx_wordcase': False, + 'thread_number': 16, + 'cloze_str': '{{c1::%s}}' + } + config.update(data) + self.check_force_update.setChecked(config.force_update) + self.check_ignore_accents.setChecked(config.ignore_accents) + self.check_ighore_mdx_wordcase.setChecked(config.ignore_mdx_wordcase) + self.input_thread_number.setValue(config.thread_number) + self.input_cloze_str.setText(config.cloze_str) + def save(self): data = { 'force_update': self.check_force_update.isChecked(), 'ignore_accents': self.check_ignore_accents.isChecked(), - # 'auto_update': self.check_auto_update.isChecked(), 'ignore_mdx_wordcase': self.check_ighore_mdx_wordcase.isChecked(), 'thread_number': self.input_thread_number.value(), 'cloze_str': self.input_cloze_str.text() } config.update(data) - \ No newline at end of file diff --git a/addons21/fastwq/libs/AnkiHub/__init__.py b/addons21/fastwq/libs/AnkiHub/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/addons21/fastwq/libs/AnkiHub/markdown2.py b/addons21/fastwq/libs/AnkiHub/markdown2.py deleted file mode 100644 index 1926792..0000000 --- a/addons21/fastwq/libs/AnkiHub/markdown2.py +++ /dev/null @@ -1,2584 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012 Trent Mick. -# Copyright (c) 2007-2008 ActiveState Corp. -# License: MIT (http://www.opensource.org/licenses/mit-license.php) - -from __future__ import generators - -r"""A fast and complete Python implementation of Markdown. - -[from http://daringfireball.net/projects/markdown/] -> Markdown is a text-to-HTML filter; it translates an easy-to-read / -> easy-to-write structured text format into HTML. Markdown's text -> format is most similar to that of plain text email, and supports -> features such as headers, *emphasis*, code blocks, blockquotes, and -> links. -> -> Markdown's syntax is designed not as a generic markup language, but -> specifically to serve as a front-end to (X)HTML. You can use span-level -> HTML tags anywhere in a Markdown document, and you can use block level -> HTML tags (like
and as well). - -Module usage: - - >>> import markdown2 - >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)` - u'

boo!

\n' - - >>> markdowner = Markdown() - >>> markdowner.convert("*boo!*") - u'

boo!

\n' - >>> markdowner.convert("**boom!**") - u'

boom!

\n' - -This implementation of Markdown implements the full "core" syntax plus a -number of extras (e.g., code syntax coloring, footnotes) as described on -. -""" - -cmdln_desc = """A fast and complete Python implementation of Markdown, a -text-to-HTML conversion tool for web writers. - -Supported extra syntax options (see -x|--extras option below and -see for details): - -* code-friendly: Disable _ and __ for em and strong. -* cuddled-lists: Allow lists to be cuddled to the preceding paragraph. -* fenced-code-blocks: Allows a code block to not have to be indented - by fencing it with '```' on a line before and after. Based on - with support for - syntax highlighting. -* footnotes: Support footnotes as in use on daringfireball.net and - implemented in other Markdown processors (tho not in Markdown.pl v1.0.1). -* numbering: Support of generic counters. Non standard extension to - allow sequential numbering of figures, tables, equations, exhibits etc. -* header-ids: Adds "id" attributes to headers. The id value is a slug of - the header text. -* html-classes: Takes a dict mapping html tag names (lowercase) to a - string to use for a "class" tag attribute. Currently only supports "img", - "table", "pre" and "code" tags. Add an issue if you require this for other - tags. -* markdown-in-html: Allow the use of `markdown="1"` in a block HTML tag to - have markdown processing be done on its contents. Similar to - but with - some limitations. -* metadata: Extract metadata from a leading '---'-fenced block. - See for details. -* nofollow: Add `rel="nofollow"` to add `` tags with an href. See - . -* pyshell: Treats unindented Python interactive shell sessions as - blocks. -* link-patterns: Auto-link given regex patterns in text (e.g. bug number - references, revision number references). -* smarty-pants: Replaces ' and " with curly quotation marks or curly - apostrophes. Replaces --, ---, ..., and . . . with en dashes, em dashes, - and ellipses. -* spoiler: A special kind of blockquote commonly hidden behind a - click on SO. Syntax per . -* toc: The returned HTML string gets a new "toc_html" attribute which is - a Table of Contents for the document. (experimental) -* xml: Passes one-liner processing instructions and namespaced XML tags. -* tables: Tables using the same format as GFM - and - PHP-Markdown Extra . -* wiki-tables: Google Code Wiki-style tables. See - . -""" - -# Dev Notes: -# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm -# not yet sure if there implications with this. Compare 'pydoc sre' -# and 'perldoc perlre'. - -__version_info__ = (2, 3, 2) -__version__ = '.'.join(map(str, __version_info__)) -__author__ = "Trent Mick" - -import sys -import re -import logging -try: - from hashlib import md5 -except ImportError: - from md5 import md5 -import optparse -from random import random, randint -import codecs -from itertools import chain - - -# ---- Python version compat - -if sys.version_info[:2] < (2, 4): - def reversed(sequence): - for i in sequence[::-1]: - yield i - -# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3). -if sys.version_info[0] <= 2: - py3 = False - try: - bytes - except NameError: - bytes = str - base_string_type = basestring -elif sys.version_info[0] >= 3: - py3 = True - unicode = str - base_string_type = str - -# ---- globals - -DEBUG = False -log = logging.getLogger("markdown") - -DEFAULT_TAB_WIDTH = 4 - - -SECRET_SALT = bytes(randint(0, 1000000)) -def _hash_text(s): - return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest() - -# Table of hash values for escaped characters: -g_escape_table = dict([(ch, _hash_text(ch)) - for ch in '\\`*_{}[]()>#+-.!']) - - -# ---- exceptions -class MarkdownError(Exception): - pass - - -# ---- public api - -def markdown_path(path, encoding="utf-8", - html4tags=False, tab_width=DEFAULT_TAB_WIDTH, - safe_mode=None, extras=None, link_patterns=None, - use_file_vars=False): - fp = codecs.open(path, 'r', encoding) - text = fp.read() - fp.close() - return Markdown(html4tags=html4tags, tab_width=tab_width, - safe_mode=safe_mode, extras=extras, - link_patterns=link_patterns, - use_file_vars=use_file_vars).convert(text) - - -def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH, - safe_mode=None, extras=None, link_patterns=None, - use_file_vars=False): - return Markdown(html4tags=html4tags, tab_width=tab_width, - safe_mode=safe_mode, extras=extras, - link_patterns=link_patterns, - use_file_vars=use_file_vars).convert(text) - - -class Markdown(object): - # The dict of "extras" to enable in processing -- a mapping of - # extra name to argument for the extra. Most extras do not have an - # argument, in which case the value is None. - # - # This can be set via (a) subclassing and (b) the constructor - # "extras" argument. - extras = None - - urls = None - titles = None - html_blocks = None - html_spans = None - html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py - - # Used to track when we're inside an ordered or unordered list - # (see _ProcessListItems() for details): - list_level = 0 - - _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) - - def __init__(self, html4tags=False, tab_width=4, safe_mode=None, - extras=None, link_patterns=None, use_file_vars=False): - if html4tags: - self.empty_element_suffix = ">" - else: - self.empty_element_suffix = " />" - self.tab_width = tab_width - - # For compatibility with earlier markdown2.py and with - # markdown.py's safe_mode being a boolean, - # safe_mode == True -> "replace" - if safe_mode is True: - self.safe_mode = "replace" - else: - self.safe_mode = safe_mode - - # Massaging and building the "extras" info. - if self.extras is None: - self.extras = {} - elif not isinstance(self.extras, dict): - self.extras = dict([(e, None) for e in self.extras]) - if extras: - if not isinstance(extras, dict): - extras = dict([(e, None) for e in extras]) - self.extras.update(extras) - assert isinstance(self.extras, dict) - if "toc" in self.extras and "header-ids" not in self.extras: - self.extras["header-ids"] = None # "toc" implies "header-ids" - self._instance_extras = self.extras.copy() - - self.link_patterns = link_patterns - self.use_file_vars = use_file_vars - self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M) - - self._escape_table = g_escape_table.copy() - if "smarty-pants" in self.extras: - self._escape_table['"'] = _hash_text('"') - self._escape_table["'"] = _hash_text("'") - - def reset(self): - self.urls = {} - self.titles = {} - self.html_blocks = {} - self.html_spans = {} - self.list_level = 0 - self.extras = self._instance_extras.copy() - if "footnotes" in self.extras: - self.footnotes = {} - self.footnote_ids = [] - if "header-ids" in self.extras: - self._count_from_header_id = {} # no `defaultdict` in Python 2.4 - if "metadata" in self.extras: - self.metadata = {} - - # Per "rel" - # should only be used in tags with an "href" attribute. - _a_nofollow = re.compile(r"<(a)([^>]*href=)", re.IGNORECASE) - - # Opens the linked document in a new window or tab - # should only used in tags with an "target" attribute. - # same with _a_nofollow - _a_blank = _a_nofollow - - def convert(self, text): - """Convert the given text.""" - # Main function. The order in which other subs are called here is - # essential. Link and image substitutions need to happen before - # _EscapeSpecialChars(), so that any *'s or _'s in the - # and tags get encoded. - - # Clear the global hashes. If we don't clear these, you get conflicts - # from other articles when generating a page which contains more than - # one article (e.g. an index page that shows the N most recent - # articles): - self.reset() - - if not isinstance(text, unicode): - # TODO: perhaps shouldn't presume UTF-8 for string input? - text = unicode(text, 'utf-8') - - if self.use_file_vars: - # Look for emacs-style file variable hints. - emacs_vars = self._get_emacs_vars(text) - if "markdown-extras" in emacs_vars: - splitter = re.compile("[ ,]+") - for e in splitter.split(emacs_vars["markdown-extras"]): - if '=' in e: - ename, earg = e.split('=', 1) - try: - earg = int(earg) - except ValueError: - pass - else: - ename, earg = e, None - self.extras[ename] = earg - - # Standardize line endings: - text = text.replace("\r\n", "\n") - text = text.replace("\r", "\n") - - # Make sure $text ends with a couple of newlines: - text += "\n\n" - - # Convert all tabs to spaces. - text = self._detab(text) - - # Strip any lines consisting only of spaces and tabs. - # This makes subsequent regexen easier to write, because we can - # match consecutive blank lines with /\n+/ instead of something - # contorted like /[ \t]*\n+/ . - text = self._ws_only_line_re.sub("", text) - - # strip metadata from head and extract - if "metadata" in self.extras: - text = self._extract_metadata(text) - - text = self.preprocess(text) - - if "fenced-code-blocks" in self.extras and not self.safe_mode: - text = self._do_fenced_code_blocks(text) - - if self.safe_mode: - text = self._hash_html_spans(text) - - # Turn block-level HTML blocks into hash entries - text = self._hash_html_blocks(text, raw=True) - - if "fenced-code-blocks" in self.extras and self.safe_mode: - text = self._do_fenced_code_blocks(text) - - # Because numbering references aren't links (yet?) then we can do everything associated with counters - # before we get started - if "numbering" in self.extras: - text = self._do_numbering(text) - - # Strip link definitions, store in hashes. - if "footnotes" in self.extras: - # Must do footnotes first because an unlucky footnote defn - # looks like a link defn: - # [^4]: this "looks like a link defn" - text = self._strip_footnote_definitions(text) - text = self._strip_link_definitions(text) - - text = self._run_block_gamut(text) - - if "footnotes" in self.extras: - text = self._add_footnotes(text) - - text = self.postprocess(text) - - text = self._unescape_special_chars(text) - - if self.safe_mode: - text = self._unhash_html_spans(text) - - if "nofollow" in self.extras: - text = self._a_nofollow.sub(r'<\1 rel="nofollow"\2', text) - - if "target-blank-links" in self.extras: - text = self._a_blank.sub(r'<\1 target="_blank"\2', text) - - text += "\n" - - rv = UnicodeWithAttrs(text) - if "toc" in self.extras: - rv._toc = self._toc - if "metadata" in self.extras: - rv.metadata = self.metadata - return rv - - def postprocess(self, text): - """A hook for subclasses to do some postprocessing of the html, if - desired. This is called before unescaping of special chars and - unhashing of raw HTML spans. - """ - return text - - def preprocess(self, text): - """A hook for subclasses to do some preprocessing of the Markdown, if - desired. This is called after basic formatting of the text, but prior - to any extras, safe mode, etc. processing. - """ - return text - - # Is metadata if the content starts with optional '---'-fenced `key: value` - # pairs. E.g. (indented for presentation): - # --- - # foo: bar - # another-var: blah blah - # --- - # # header - # or: - # foo: bar - # another-var: blah blah - # - # # header - _meta_data_pattern = re.compile(r'^(?:---[\ \t]*\n)?(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)|([\S\w]+\s*:(?! >)[ \t]*.*\n?)(?:---[\ \t]*\n)?', re.MULTILINE) - _key_val_pat = re.compile("[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE) - # this allows key: > - # value - # conutiues over multiple lines - _key_val_block_pat = re.compile( - "(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)", re.MULTILINE) - - def _extract_metadata(self, text): - match = re.findall(self._meta_data_pattern, text) - - if not match: - return text - - last_item = list(filter(None, match[-1]))[0] - end_of_metadata = text.index(last_item)+len(last_item) - if text.startswith("---"): - # add 8 charachters for opening and closing - # and since indexing starts at 0 we add a step - tail = text[end_of_metadata+4:] - else: - tail = text[end_of_metadata:] - - kv = re.findall(self._key_val_pat, text) - kvm = re.findall(self._key_val_block_pat, text) - kvm = [item.replace(": >\n", ":", 1) for item in kvm] - - for item in kv + kvm: - k, v = item.split(":", 1) - self.metadata[k.strip()] = v.strip() - - return tail - - _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE) - # This regular expression is intended to match blocks like this: - # PREFIX Local Variables: SUFFIX - # PREFIX mode: Tcl SUFFIX - # PREFIX End: SUFFIX - # Some notes: - # - "[ \t]" is used instead of "\s" to specifically exclude newlines - # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does - # not like anything other than Unix-style line terminators. - _emacs_local_vars_pat = re.compile(r"""^ - (?P(?:[^\r\n|\n|\r])*?) - [\ \t]*Local\ Variables:[\ \t]* - (?P.*?)(?:\r\n|\n|\r) - (?P.*?\1End:) - """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE) - - def _get_emacs_vars(self, text): - """Return a dictionary of emacs-style local variables. - - Parsing is done loosely according to this spec (and according to - some in-practice deviations from this): - http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables - """ - emacs_vars = {} - SIZE = pow(2, 13) # 8kB - - # Search near the start for a '-*-'-style one-liner of variables. - head = text[:SIZE] - if "-*-" in head: - match = self._emacs_oneliner_vars_pat.search(head) - if match: - emacs_vars_str = match.group(1) - assert '\n' not in emacs_vars_str - emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';') - if s.strip()] - if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]: - # While not in the spec, this form is allowed by emacs: - # -*- Tcl -*- - # where the implied "variable" is "mode". This form - # is only allowed if there are no other variables. - emacs_vars["mode"] = emacs_var_strs[0].strip() - else: - for emacs_var_str in emacs_var_strs: - try: - variable, value = emacs_var_str.strip().split(':', 1) - except ValueError: - log.debug("emacs variables error: malformed -*- " - "line: %r", emacs_var_str) - continue - # Lowercase the variable name because Emacs allows "Mode" - # or "mode" or "MoDe", etc. - emacs_vars[variable.lower()] = value.strip() - - tail = text[-SIZE:] - if "Local Variables" in tail: - match = self._emacs_local_vars_pat.search(tail) - if match: - prefix = match.group("prefix") - suffix = match.group("suffix") - lines = match.group("content").splitlines(0) - # print "prefix=%r, suffix=%r, content=%r, lines: %s"\ - # % (prefix, suffix, match.group("content"), lines) - - # Validate the Local Variables block: proper prefix and suffix - # usage. - for i, line in enumerate(lines): - if not line.startswith(prefix): - log.debug("emacs variables error: line '%s' " - "does not use proper prefix '%s'" - % (line, prefix)) - return {} - # Don't validate suffix on last line. Emacs doesn't care, - # neither should we. - if i != len(lines)-1 and not line.endswith(suffix): - log.debug("emacs variables error: line '%s' " - "does not use proper suffix '%s'" - % (line, suffix)) - return {} - - # Parse out one emacs var per line. - continued_for = None - for line in lines[:-1]: # no var on the last line ("PREFIX End:") - if prefix: line = line[len(prefix):] # strip prefix - if suffix: line = line[:-len(suffix)] # strip suffix - line = line.strip() - if continued_for: - variable = continued_for - if line.endswith('\\'): - line = line[:-1].rstrip() - else: - continued_for = None - emacs_vars[variable] += ' ' + line - else: - try: - variable, value = line.split(':', 1) - except ValueError: - log.debug("local variables error: missing colon " - "in local variables entry: '%s'" % line) - continue - # Do NOT lowercase the variable name, because Emacs only - # allows "mode" (and not "Mode", "MoDe", etc.) in this block. - value = value.strip() - if value.endswith('\\'): - value = value[:-1].rstrip() - continued_for = variable - else: - continued_for = None - emacs_vars[variable] = value - - # Unquote values. - for var, val in list(emacs_vars.items()): - if len(val) > 1 and (val.startswith('"') and val.endswith('"') - or val.startswith('"') and val.endswith('"')): - emacs_vars[var] = val[1:-1] - - return emacs_vars - - def _detab_line(self, line): - r"""Recusively convert tabs to spaces in a single line. - - Called from _detab().""" - if '\t' not in line: - return line - chunk1, chunk2 = line.split('\t', 1) - chunk1 += (' ' * (self.tab_width - len(chunk1) % self.tab_width)) - output = chunk1 + chunk2 - return self._detab_line(output) - - def _detab(self, text): - r"""Iterate text line by line and convert tabs to spaces. - - >>> m = Markdown() - >>> m._detab("\tfoo") - ' foo' - >>> m._detab(" \tfoo") - ' foo' - >>> m._detab("\t foo") - ' foo' - >>> m._detab(" foo") - ' foo' - >>> m._detab(" foo\n\tbar\tblam") - ' foo\n bar blam' - """ - if '\t' not in text: - return text - output = [] - for line in text.splitlines(): - output.append(self._detab_line(line)) - return '\n'.join(output) - - # I broke out the html5 tags here and add them to _block_tags_a and - # _block_tags_b. This way html5 tags are easy to keep track of. - _html5tags = '|article|aside|header|hgroup|footer|nav|section|figure|figcaption' - - _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del' - _block_tags_a += _html5tags - - _strict_tag_block_re = re.compile(r""" - ( # save in \1 - ^ # start of line (with re.M) - <(%s) # start tag = \2 - \b # word break - (.*\n)*? # any number of lines, minimally matching - # the matching end tag - [ \t]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - ) - """ % _block_tags_a, - re.X | re.M) - - _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math' - _block_tags_b += _html5tags - - _liberal_tag_block_re = re.compile(r""" - ( # save in \1 - ^ # start of line (with re.M) - <(%s) # start tag = \2 - \b # word break - (.*\n)*? # any number of lines, minimally matching - .* # the matching end tag - [ \t]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - ) - """ % _block_tags_b, - re.X | re.M) - - _html_markdown_attr_re = re.compile( - r'''\s+markdown=("1"|'1')''') - def _hash_html_block_sub(self, match, raw=False): - html = match.group(1) - if raw and self.safe_mode: - html = self._sanitize_html(html) - elif 'markdown-in-html' in self.extras and 'markdown=' in html: - first_line = html.split('\n', 1)[0] - m = self._html_markdown_attr_re.search(first_line) - if m: - lines = html.split('\n') - middle = '\n'.join(lines[1:-1]) - last_line = lines[-1] - first_line = first_line[:m.start()] + first_line[m.end():] - f_key = _hash_text(first_line) - self.html_blocks[f_key] = first_line - l_key = _hash_text(last_line) - self.html_blocks[l_key] = last_line - return ''.join(["\n\n", f_key, - "\n\n", middle, "\n\n", - l_key, "\n\n"]) - key = _hash_text(html) - self.html_blocks[key] = html - return "\n\n" + key + "\n\n" - - def _hash_html_blocks(self, text, raw=False): - """Hashify HTML blocks - - We only want to do this for block-level HTML tags, such as headers, - lists, and tables. That's because we still want to wrap

s around - "paragraphs" that are wrapped in non-block-level tags, such as anchors, - phrase emphasis, and spans. The list of tags we're looking for is - hard-coded. - - @param raw {boolean} indicates if these are raw HTML blocks in - the original source. It makes a difference in "safe" mode. - """ - if '<' not in text: - return text - - # Pass `raw` value into our calls to self._hash_html_block_sub. - hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw) - - # First, look for nested blocks, e.g.: - #

- #
- # tags for inner block must be indented. - #
- #
- # - # The outermost tags must start at the left margin for this to match, and - # the inner nested divs must be indented. - # We need to do this before the next, more liberal match, because the next - # match will start at the first `
` and stop at the first `
`. - text = self._strict_tag_block_re.sub(hash_html_block_sub, text) - - # Now match more liberally, simply from `\n` to `\n` - text = self._liberal_tag_block_re.sub(hash_html_block_sub, text) - - # Special case just for
. It was easier to make a special - # case than to make the other regex more complicated. - if "", start_idx) + 3 - except ValueError: - break - - # Start position for next comment block search. - start = end_idx - - # Validate whitespace before comment. - if start_idx: - # - Up to `tab_width - 1` spaces before start_idx. - for i in range(self.tab_width - 1): - if text[start_idx - 1] != ' ': - break - start_idx -= 1 - if start_idx == 0: - break - # - Must be preceded by 2 newlines or hit the start of - # the document. - if start_idx == 0: - pass - elif start_idx == 1 and text[0] == '\n': - start_idx = 0 # to match minute detail of Markdown.pl regex - elif text[start_idx-2:start_idx] == '\n\n': - pass - else: - break - - # Validate whitespace after comment. - # - Any number of spaces and tabs. - while end_idx < len(text): - if text[end_idx] not in ' \t': - break - end_idx += 1 - # - Must be following by 2 newlines or hit end of text. - if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'): - continue - - # Escape and hash (must match `_hash_html_block_sub`). - html = text[start_idx:end_idx] - if raw and self.safe_mode: - html = self._sanitize_html(html) - key = _hash_text(html) - self.html_blocks[key] = html - text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:] - - if "xml" in self.extras: - # Treat XML processing instructions and namespaced one-liner - # tags as if they were block HTML tags. E.g., if standalone - # (i.e. are their own paragraph), the following do not get - # wrapped in a

tag: - # - # - # - _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width) - text = _xml_oneliner_re.sub(hash_html_block_sub, text) - - return text - - def _strip_link_definitions(self, text): - # Strips link definitions from text, stores the URLs and titles in - # hash references. - less_than_tab = self.tab_width - 1 - - # Link defs are in the form: - # [id]: url "optional title" - _link_def_re = re.compile(r""" - ^[ ]{0,%d}\[(.+)\]: # id = \1 - [ \t]* - \n? # maybe *one* newline - [ \t]* - ? # url = \2 - [ \t]* - (?: - \n? # maybe one newline - [ \t]* - (?<=\s) # lookbehind for whitespace - ['"(] - ([^\n]*) # title = \3 - ['")] - [ \t]* - )? # title is optional - (?:\n+|\Z) - """ % less_than_tab, re.X | re.M | re.U) - return _link_def_re.sub(self._extract_link_def_sub, text) - - def _extract_link_def_sub(self, match): - id, url, title = match.groups() - key = id.lower() # Link IDs are case-insensitive - self.urls[key] = self._encode_amps_and_angles(url) - if title: - self.titles[key] = title - return "" - - def _do_numbering(self, text): - ''' We handle the special extension for generic numbering for - tables, figures etc. - ''' - # First pass to define all the references - self.regex_defns = re.compile(r''' - \[\#(\w+)\s* # the counter. Open square plus hash plus a word \1 - ([^@]*)\s* # Some optional characters, that aren't an @. \2 - @(\w+) # the id. Should this be normed? \3 - ([^\]]*)\] # The rest of the text up to the terminating ] \4 - ''', re.VERBOSE) - self.regex_subs = re.compile(r"\[@(\w+)\s*\]") # [@ref_id] - counters = {} - references = {} - replacements = [] - definition_html = '

{}{}{}
' - reference_html = '
{}' - for match in self.regex_defns.finditer(text): - # We must have four match groups otherwise this isn't a numbering reference - if len(match.groups()) != 4: - continue - counter = match.group(1) - text_before = match.group(2) - ref_id = match.group(3) - text_after = match.group(4) - number = counters.get(counter, 1) - references[ref_id] = (number, counter) - replacements.append((match.start(0), - definition_html.format(counter, - ref_id, - text_before, - number, - text_after), - match.end(0))) - counters[counter] = number + 1 - for repl in reversed(replacements): - text = text[:repl[0]] + repl[1] + text[repl[2]:] - - # Second pass to replace the references with the right - # value of the counter - # Fwiw, it's vaguely annoying to have to turn the iterator into - # a list and then reverse it but I can't think of a better thing to do. - for match in reversed(list(self.regex_subs.finditer(text))): - number, counter = references.get(match.group(1), (None, None)) - if number is not None: - repl = reference_html.format(counter, - match.group(1), - number) - else: - repl = reference_html.format(match.group(1), - 'countererror', - '?' + match.group(1) + '?') - if "smarty-pants" in self.extras: - repl = repl.replace('"', self._escape_table['"']) - - text = text[:match.start()] + repl + text[match.end():] - return text - - def _extract_footnote_def_sub(self, match): - id, text = match.groups() - text = _dedent(text, skip_first_line=not text.startswith('\n')).strip() - normed_id = re.sub(r'\W', '-', id) - # Ensure footnote text ends with a couple newlines (for some - # block gamut matches). - self.footnotes[normed_id] = text + "\n\n" - return "" - - def _strip_footnote_definitions(self, text): - """A footnote definition looks like this: - - [^note-id]: Text of the note. - - May include one or more indented paragraphs. - - Where, - - The 'note-id' can be pretty much anything, though typically it - is the number of the footnote. - - The first paragraph may start on the next line, like so: - - [^note-id]: - Text of the note. - """ - less_than_tab = self.tab_width - 1 - footnote_def_re = re.compile(r''' - ^[ ]{0,%d}\[\^(.+)\]: # id = \1 - [ \t]* - ( # footnote text = \2 - # First line need not start with the spaces. - (?:\s*.*\n+) - (?: - (?:[ ]{%d} | \t) # Subsequent lines must be indented. - .*\n+ - )* - ) - # Lookahead for non-space at line-start, or end of doc. - (?:(?=^[ ]{0,%d}\S)|\Z) - ''' % (less_than_tab, self.tab_width, self.tab_width), - re.X | re.M) - return footnote_def_re.sub(self._extract_footnote_def_sub, text) - - _hr_re = re.compile(r'^[ ]{0,3}([-_*][ ]{0,2}){3,}$', re.M) - - def _run_block_gamut(self, text): - # These are all the transformations that form block-level - # tags like paragraphs, headers, and list items. - - if "fenced-code-blocks" in self.extras: - text = self._do_fenced_code_blocks(text) - - text = self._do_headers(text) - - # Do Horizontal Rules: - # On the number of spaces in horizontal rules: The spec is fuzzy: "If - # you wish, you may use spaces between the hyphens or asterisks." - # Markdown.pl 1.0.1's hr regexes limit the number of spaces between the - # hr chars to one or two. We'll reproduce that limit here. - hr = "\n tags around block-level tags. - text = self._hash_html_blocks(text) - - text = self._form_paragraphs(text) - - return text - - def _pyshell_block_sub(self, match): - lines = match.group(0).splitlines(0) - _dedentlines(lines) - indent = ' ' * self.tab_width - s = ('\n' # separate from possible cuddled paragraph - + indent + ('\n'+indent).join(lines) - + '\n\n') - return s - - def _prepare_pyshell_blocks(self, text): - """Ensure that Python interactive shell sessions are put in - code blocks -- even if not properly indented. - """ - if ">>>" not in text: - return text - - less_than_tab = self.tab_width - 1 - _pyshell_block_re = re.compile(r""" - ^([ ]{0,%d})>>>[ ].*\n # first line - ^(\1.*\S+.*\n)* # any number of subsequent lines - ^\n # ends with a blank line - """ % less_than_tab, re.M | re.X) - - return _pyshell_block_re.sub(self._pyshell_block_sub, text) - - def _table_sub(self, match): - trim_space_re = '^[ \t\n]+|[ \t\n]+$' - trim_bar_re = '^\||\|$' - - head, underline, body = match.groups() - - # Determine aligns for columns. - cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", underline)).split('|')] - align_from_col_idx = {} - for col_idx, col in enumerate(cols): - if col[0] == ':' and col[-1] == ':': - align_from_col_idx[col_idx] = ' align="center"' - elif col[0] == ':': - align_from_col_idx[col_idx] = ' align="left"' - elif col[-1] == ':': - align_from_col_idx[col_idx] = ' align="right"' - - # thead - hlines = ['' % self._html_class_str_from_tag('table'), '
', ''] - cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)).split('|')] - for col_idx, col in enumerate(cols): - hlines.append(' %s' % ( - align_from_col_idx.get(col_idx, ''), - self._run_span_gamut(col) - )) - hlines.append('') - hlines.append('') - - # tbody - hlines.append('') - for line in body.strip('\n').split('\n'): - hlines.append('') - cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)).split('|')] - for col_idx, col in enumerate(cols): - hlines.append(' %s' % ( - align_from_col_idx.get(col_idx, ''), - self._run_span_gamut(col) - )) - hlines.append('') - hlines.append('') - hlines.append('
') - - return '\n'.join(hlines) + '\n' - - def _do_tables(self, text): - """Copying PHP-Markdown and GFM table syntax. Some regex borrowed from - https://github.com/michelf/php-markdown/blob/lib/Michelf/Markdown.php#L2538 - """ - less_than_tab = self.tab_width - 1 - table_re = re.compile(r''' - (?:(?<=\n\n)|\A\n?) # leading blank line - - ^[ ]{0,%d} # allowed whitespace - (.*[|].*) \n # $1: header row (at least one pipe) - - ^[ ]{0,%d} # allowed whitespace - ( # $2: underline row - # underline row with leading bar - (?: \|\ *:?-+:?\ * )+ \|? \n - | - # or, underline row without leading bar - (?: \ *:?-+:?\ *\| )+ (?: \ *:?-+:?\ * )? \n - ) - - ( # $3: data rows - (?: - ^[ ]{0,%d}(?!\ ) # ensure line begins with 0 to less_than_tab spaces - .*\|.* \n - )+ - ) - ''' % (less_than_tab, less_than_tab, less_than_tab), re.M | re.X) - return table_re.sub(self._table_sub, text) - - def _wiki_table_sub(self, match): - ttext = match.group(0).strip() - # print 'wiki table: %r' % match.group(0) - rows = [] - for line in ttext.splitlines(0): - line = line.strip()[2:-2].strip() - row = [c.strip() for c in re.split(r'(?' % self._html_class_str_from_tag('table'), ''] - for row in rows: - hrow = [''] - for cell in row: - hrow.append('') - hrow.append(self._run_span_gamut(cell)) - hrow.append('') - hrow.append('') - hlines.append(''.join(hrow)) - hlines += ['', ''] - return '\n'.join(hlines) + '\n' - - def _do_wiki_tables(self, text): - # Optimization. - if "||" not in text: - return text - - less_than_tab = self.tab_width - 1 - wiki_table_re = re.compile(r''' - (?:(?<=\n\n)|\A\n?) # leading blank line - ^([ ]{0,%d})\|\|.+?\|\|[ ]*\n # first line - (^\1\|\|.+?\|\|\n)* # any number of subsequent lines - ''' % less_than_tab, re.M | re.X) - return wiki_table_re.sub(self._wiki_table_sub, text) - - def _run_span_gamut(self, text): - # These are all the transformations that occur *within* block-level - # tags like paragraphs, headers, and list items. - - text = self._do_code_spans(text) - - text = self._escape_special_chars(text) - - # Process anchor and image tags. - text = self._do_links(text) - - # Make links out of things like `` - # Must come after _do_links(), because you can use < and > - # delimiters in inline links like [this](). - text = self._do_auto_links(text) - - if "link-patterns" in self.extras: - text = self._do_link_patterns(text) - - text = self._encode_amps_and_angles(text) - - if "strike" in self.extras: - text = self._do_strike(text) - - text = self._do_italics_and_bold(text) - - if "smarty-pants" in self.extras: - text = self._do_smart_punctuation(text) - - # Do hard breaks: - if "break-on-newline" in self.extras: - text = re.sub(r" *\n", " - | - # auto-link (e.g., ) - <\w+[^>]*> - | - # comment - | - <\?.*?\?> # processing instruction - ) - """, re.X) - - def _escape_special_chars(self, text): - # Python markdown note: the HTML tokenization here differs from - # that in Markdown.pl, hence the behaviour for subtle cases can - # differ (I believe the tokenizer here does a better job because - # it isn't susceptible to unmatched '<' and '>' in HTML tags). - # Note, however, that '>' is not allowed in an auto-link URL - # here. - escaped = [] - is_html_markup = False - for token in self._sorta_html_tokenize_re.split(text): - if is_html_markup: - # Within tags/HTML-comments/auto-links, encode * and _ - # so they don't conflict with their use in Markdown for - # italics and strong. We're replacing each such - # character with its corresponding MD5 checksum value; - # this is likely overkill, but it should prevent us from - # colliding with the escape values by accident. - escaped.append(token.replace('*', self._escape_table['*']) - .replace('_', self._escape_table['_'])) - else: - escaped.append(self._encode_backslash_escapes(token)) - is_html_markup = not is_html_markup - return ''.join(escaped) - - def _hash_html_spans(self, text): - # Used for safe_mode. - - def _is_auto_link(s): - if ':' in s and self._auto_link_re.match(s): - return True - elif '@' in s and self._auto_email_link_re.match(s): - return True - return False - - tokens = [] - is_html_markup = False - for token in self._sorta_html_tokenize_re.split(text): - if is_html_markup and not _is_auto_link(token): - sanitized = self._sanitize_html(token) - key = _hash_text(sanitized) - self.html_spans[key] = sanitized - tokens.append(key) - else: - tokens.append(token) - is_html_markup = not is_html_markup - return ''.join(tokens) - - def _unhash_html_spans(self, text): - for key, sanitized in list(self.html_spans.items()): - text = text.replace(key, sanitized) - return text - - def _sanitize_html(self, s): - if self.safe_mode == "replace": - return self.html_removed_text - elif self.safe_mode == "escape": - replacements = [ - ('&', '&'), - ('<', '<'), - ('>', '>'), - ] - for before, after in replacements: - s = s.replace(before, after) - return s - else: - raise MarkdownError("invalid value for 'safe_mode': %r (must be " - "'escape' or 'replace')" % self.safe_mode) - - _inline_link_title = re.compile(r''' - ( # \1 - [ \t]+ - (['"]) # quote char = \2 - (?P.*?) - \2 - )? # title is optional - \)$ - ''', re.X | re.S) - _tail_of_reference_link_re = re.compile(r''' - # Match tail of: [text][id] - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - \[ - (?P<id>.*?) - \] - ''', re.X | re.S) - - _whitespace = re.compile(r'\s*') - - _strip_anglebrackets = re.compile(r'<(.*)>.*') - - def _find_non_whitespace(self, text, start): - """Returns the index of the first non-whitespace character in text - after (and including) start - """ - match = self._whitespace.match(text, start) - return match.end() - - def _find_balanced(self, text, start, open_c, close_c): - """Returns the index where the open_c and close_c characters balance - out - the same number of open_c and close_c are encountered - or the - end of string if it's reached before the balance point is found. - """ - i = start - l = len(text) - count = 1 - while count > 0 and i < l: - if text[i] == open_c: - count += 1 - elif text[i] == close_c: - count -= 1 - i += 1 - return i - - def _extract_url_and_title(self, text, start): - """Extracts the url and (optional) title from the tail of a link""" - # text[start] equals the opening parenthesis - idx = self._find_non_whitespace(text, start+1) - if idx == len(text): - return None, None, None - end_idx = idx - has_anglebrackets = text[idx] == "<" - if has_anglebrackets: - end_idx = self._find_balanced(text, end_idx+1, "<", ">") - end_idx = self._find_balanced(text, end_idx, "(", ")") - match = self._inline_link_title.search(text, idx, end_idx) - if not match: - return None, None, None - url, title = text[idx:match.start()], match.group("title") - if has_anglebrackets: - url = self._strip_anglebrackets.sub(r'\1', url) - return url, title, end_idx - - def _do_links(self, text): - """Turn Markdown link shortcuts into XHTML <a> and <img> tags. - - This is a combination of Markdown.pl's _DoAnchors() and - _DoImages(). They are done together because that simplified the - approach. It was necessary to use a different approach than - Markdown.pl because of the lack of atomic matching support in - Python's regex engine used in $g_nested_brackets. - """ - MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24 - - # `anchor_allowed_pos` is used to support img links inside - # anchors, but not anchors inside anchors. An anchor's start - # pos must be `>= anchor_allowed_pos`. - anchor_allowed_pos = 0 - - curr_pos = 0 - while True: # Handle the next link. - # The next '[' is the start of: - # - an inline anchor: [text](url "title") - # - a reference anchor: [text][id] - # - an inline img: ![text](url "title") - # - a reference img: ![text][id] - # - a footnote ref: [^id] - # (Only if 'footnotes' extra enabled) - # - a footnote defn: [^id]: ... - # (Only if 'footnotes' extra enabled) These have already - # been stripped in _strip_footnote_definitions() so no - # need to watch for them. - # - a link definition: [id]: url "title" - # These have already been stripped in - # _strip_link_definitions() so no need to watch for them. - # - not markup: [...anything else... - try: - start_idx = text.index('[', curr_pos) - except ValueError: - break - text_length = len(text) - - # Find the matching closing ']'. - # Markdown.pl allows *matching* brackets in link text so we - # will here too. Markdown.pl *doesn't* currently allow - # matching brackets in img alt text -- we'll differ in that - # regard. - bracket_depth = 0 - for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL, - text_length)): - ch = text[p] - if ch == ']': - bracket_depth -= 1 - if bracket_depth < 0: - break - elif ch == '[': - bracket_depth += 1 - else: - # Closing bracket not found within sentinel length. - # This isn't markup. - curr_pos = start_idx + 1 - continue - link_text = text[start_idx+1:p] - - # Possibly a footnote ref? - if "footnotes" in self.extras and link_text.startswith("^"): - normed_id = re.sub(r'\W', '-', link_text[1:]) - if normed_id in self.footnotes: - self.footnote_ids.append(normed_id) - result = '<sup class="footnote-ref" id="fnref-%s">' \ - '<a href="#fn-%s">%s</a></sup>' \ - % (normed_id, normed_id, len(self.footnote_ids)) - text = text[:start_idx] + result + text[p+1:] - else: - # This id isn't defined, leave the markup alone. - curr_pos = p+1 - continue - - # Now determine what this is by the remainder. - p += 1 - if p == text_length: - return text - - # Inline anchor or img? - if text[p] == '(': # attempt at perf improvement - url, title, url_end_idx = self._extract_url_and_title(text, p) - if url is not None: - # Handle an inline anchor or img. - is_img = start_idx > 0 and text[start_idx-1] == "!" - if is_img: - start_idx -= 1 - - # We've got to encode these to avoid conflicting - # with italics/bold. - url = url.replace('*', self._escape_table['*']) \ - .replace('_', self._escape_table['_']) - if title: - title_str = ' title="%s"' % ( - _xml_escape_attr(title) - .replace('*', self._escape_table['*']) - .replace('_', self._escape_table['_'])) - else: - title_str = '' - if is_img: - img_class_str = self._html_class_str_from_tag("img") - result = '<img src="%s" alt="%s"%s%s%s' \ - % (url.replace('"', '"'), - _xml_escape_attr(link_text), - title_str, img_class_str, self.empty_element_suffix) - if "smarty-pants" in self.extras: - result = result.replace('"', self._escape_table['"']) - curr_pos = start_idx + len(result) - text = text[:start_idx] + result + text[url_end_idx:] - elif start_idx >= anchor_allowed_pos: - result_head = '<a href="%s"%s>' % (url, title_str) - result = '%s%s</a>' % (result_head, link_text) - if "smarty-pants" in self.extras: - result = result.replace('"', self._escape_table['"']) - # <img> allowed from curr_pos on, <a> from - # anchor_allowed_pos on. - curr_pos = start_idx + len(result_head) - anchor_allowed_pos = start_idx + len(result) - text = text[:start_idx] + result + text[url_end_idx:] - else: - # Anchor not allowed here. - curr_pos = start_idx + 1 - continue - - # Reference anchor or img? - else: - match = self._tail_of_reference_link_re.match(text, p) - if match: - # Handle a reference-style anchor or img. - is_img = start_idx > 0 and text[start_idx-1] == "!" - if is_img: - start_idx -= 1 - link_id = match.group("id").lower() - if not link_id: - link_id = link_text.lower() # for links like [this][] - if link_id in self.urls: - url = self.urls[link_id] - # We've got to encode these to avoid conflicting - # with italics/bold. - url = url.replace('*', self._escape_table['*']) \ - .replace('_', self._escape_table['_']) - title = self.titles.get(link_id) - if title: - title = _xml_escape_attr(title) \ - .replace('*', self._escape_table['*']) \ - .replace('_', self._escape_table['_']) - title_str = ' title="%s"' % title - else: - title_str = '' - if is_img: - img_class_str = self._html_class_str_from_tag("img") - result = '<img src="%s" alt="%s"%s%s%s' \ - % (url.replace('"', '"'), - link_text.replace('"', '"'), - title_str, img_class_str, self.empty_element_suffix) - if "smarty-pants" in self.extras: - result = result.replace('"', self._escape_table['"']) - curr_pos = start_idx + len(result) - text = text[:start_idx] + result + text[match.end():] - elif start_idx >= anchor_allowed_pos: - result = '<a href="%s"%s>%s</a>' \ - % (url, title_str, link_text) - result_head = '<a href="%s"%s>' % (url, title_str) - result = '%s%s</a>' % (result_head, link_text) - if "smarty-pants" in self.extras: - result = result.replace('"', self._escape_table['"']) - # <img> allowed from curr_pos on, <a> from - # anchor_allowed_pos on. - curr_pos = start_idx + len(result_head) - anchor_allowed_pos = start_idx + len(result) - text = text[:start_idx] + result + text[match.end():] - else: - # Anchor not allowed here. - curr_pos = start_idx + 1 - else: - # This id isn't defined, leave the markup alone. - curr_pos = match.end() - continue - - # Otherwise, it isn't markup. - curr_pos = start_idx + 1 - - return text - - def header_id_from_text(self, text, prefix, n): - """Generate a header id attribute value from the given header - HTML content. - - This is only called if the "header-ids" extra is enabled. - Subclasses may override this for different header ids. - - @param text {str} The text of the header tag - @param prefix {str} The requested prefix for header ids. This is the - value of the "header-ids" extra key, if any. Otherwise, None. - @param n {int} The <hN> tag number, i.e. `1` for an <h1> tag. - @returns {str} The value for the header tag's "id" attribute. Return - None to not have an id attribute and to exclude this header from - the TOC (if the "toc" extra is specified). - """ - header_id = _slugify(text) - if prefix and isinstance(prefix, base_string_type): - header_id = prefix + '-' + header_id - if header_id in self._count_from_header_id: - self._count_from_header_id[header_id] += 1 - header_id += '-%s' % self._count_from_header_id[header_id] - else: - self._count_from_header_id[header_id] = 1 - return header_id - - _toc = None - def _toc_add_entry(self, level, id, name): - if self._toc is None: - self._toc = [] - self._toc.append((level, id, self._unescape_special_chars(name))) - - _h_re_base = r''' - (^(.+)[ \t]*\n(=+|-+)[ \t]*\n+) - | - (^(\#{1,6}) # \1 = string of #'s - [ \t]%s - (.+?) # \2 = Header text - [ \t]* - (?<!\\) # ensure not an escaped trailing '#' - \#* # optional closing #'s (not counted) - \n+ - ) - ''' - - _h_re = re.compile(_h_re_base % '*', re.X | re.M) - _h_re_tag_friendly = re.compile(_h_re_base % '+', re.X | re.M) - - def _h_sub(self, match): - if match.group(1) is not None: - # Setext header - n = {"=": 1, "-": 2}[match.group(3)[0]] - header_group = match.group(2) - else: - # atx header - n = len(match.group(5)) - header_group = match.group(6) - - demote_headers = self.extras.get("demote-headers") - if demote_headers: - n = min(n + demote_headers, 6) - header_id_attr = "" - if "header-ids" in self.extras: - header_id = self.header_id_from_text(header_group, - self.extras["header-ids"], n) - if header_id: - header_id_attr = ' id="%s"' % header_id - html = self._run_span_gamut(header_group) - if "toc" in self.extras and header_id: - self._toc_add_entry(n, header_id, html) - return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n) - - def _do_headers(self, text): - # Setext-style headers: - # Header 1 - # ======== - # - # Header 2 - # -------- - - # atx-style headers: - # # Header 1 - # ## Header 2 - # ## Header 2 with closing hashes ## - # ... - # ###### Header 6 - - if 'tag-friendly' in self.extras: - return self._h_re_tag_friendly.sub(self._h_sub, text) - return self._h_re.sub(self._h_sub, text) - - _marker_ul_chars = '*+-' - _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars - _marker_ul = '(?:[%s])' % _marker_ul_chars - _marker_ol = r'(?:\d+\.)' - - def _list_sub(self, match): - lst = match.group(1) - lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol" - result = self._process_list_items(lst) - if self.list_level: - return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type) - else: - return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type) - - def _do_lists(self, text): - # Form HTML ordered (numbered) and unordered (bulleted) lists. - - # Iterate over each *non-overlapping* list match. - pos = 0 - while True: - # Find the *first* hit for either list style (ul or ol). We - # match ul and ol separately to avoid adjacent lists of different - # types running into each other (see issue #16). - hits = [] - for marker_pat in (self._marker_ul, self._marker_ol): - less_than_tab = self.tab_width - 1 - whole_list = r''' - ( # \1 = whole list - ( # \2 - [ ]{0,%d} - (%s) # \3 = first list item marker - [ \t]+ - (?!\ *\3\ ) # '- - - ...' isn't a list. See 'not_quite_a_list' test case. - ) - (?:.+?) - ( # \4 - \Z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another list item marker - [ \t]* - %s[ \t]+ - ) - ) - ) - ''' % (less_than_tab, marker_pat, marker_pat) - if self.list_level: # sub-list - list_re = re.compile("^"+whole_list, re.X | re.M | re.S) - else: - list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list, - re.X | re.M | re.S) - match = list_re.search(text, pos) - if match: - hits.append((match.start(), match)) - if not hits: - break - hits.sort() - match = hits[0][1] - start, end = match.span() - middle = self._list_sub(match) - text = text[:start] + middle + text[end:] - pos = start + len(middle) # start pos for next attempted match - - return text - - _list_item_re = re.compile(r''' - (\n)? # leading line = \1 - (^[ \t]*) # leading whitespace = \2 - (?P<marker>%s) [ \t]+ # list marker = \3 - ((?:.+?) # list item text = \4 - (\n{1,2})) # eols = \5 - (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+)) - ''' % (_marker_any, _marker_any), - re.M | re.X | re.S) - - _task_list_item_re = re.compile(r''' - (\[[\ x]\])[ \t]+ # tasklist marker = \1 - (.*) # list item text = \2 - ''', re.M | re.X | re.S) - - _task_list_warpper_str = r'<p><input type="checkbox" class="task-list-item-checkbox" %sdisabled>%s</p>' - - def _task_list_item_sub(self, match): - marker = match.group(1) - item_text = match.group(2) - if marker == '[x]': - return self._task_list_warpper_str % ('checked ', item_text) - elif marker == '[ ]': - return self._task_list_warpper_str % ('', item_text) - - _last_li_endswith_two_eols = False - def _list_item_sub(self, match): - item = match.group(4) - leading_line = match.group(1) - if leading_line or "\n\n" in item or self._last_li_endswith_two_eols: - item = self._run_block_gamut(self._outdent(item)) - else: - # Recursion for sub-lists: - item = self._do_lists(self._outdent(item)) - if item.endswith('\n'): - item = item[:-1] - item = self._run_span_gamut(item) - self._last_li_endswith_two_eols = (len(match.group(5)) == 2) - - if "task_list" in self.extras: - item = self._task_list_item_re.sub(self._task_list_item_sub, item) - - return "<li>%s</li>\n" % item - - def _process_list_items(self, list_str): - # Process the contents of a single ordered or unordered list, - # splitting it into individual list items. - - # The $g_list_level global keeps track of when we're inside a list. - # Each time we enter a list, we increment it; when we leave a list, - # we decrement. If it's zero, we're not in a list anymore. - # - # We do this because when we're not inside a list, we want to treat - # something like this: - # - # I recommend upgrading to version - # 8. Oops, now this line is treated - # as a sub-list. - # - # As a single paragraph, despite the fact that the second line starts - # with a digit-period-space sequence. - # - # Whereas when we're inside a list (or sub-list), that line will be - # treated as the start of a sub-list. What a kludge, huh? This is - # an aspect of Markdown's syntax that's hard to parse perfectly - # without resorting to mind-reading. Perhaps the solution is to - # change the syntax rules such that sub-lists must start with a - # starting cardinal number; e.g. "1." or "a.". - self.list_level += 1 - self._last_li_endswith_two_eols = False - list_str = list_str.rstrip('\n') + '\n' - list_str = self._list_item_re.sub(self._list_item_sub, list_str) - self.list_level -= 1 - return list_str - - def _get_pygments_lexer(self, lexer_name): - try: - from pygments import lexers, util - except ImportError: - return None - try: - return lexers.get_lexer_by_name(lexer_name) - except util.ClassNotFound: - return None - - def _color_with_pygments(self, codeblock, lexer, **formatter_opts): - import pygments - import pygments.formatters - - class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): - def _wrap_code(self, inner): - """A function for use in a Pygments Formatter which - wraps in <code> tags. - """ - yield 0, "<code>" - for tup in inner: - yield tup - yield 0, "</code>" - - def wrap(self, source, outfile): - """Return the source with a code, pre, and div.""" - return self._wrap_div(self._wrap_pre(self._wrap_code(source))) - - formatter_opts.setdefault("cssclass", "codehilite") - formatter = HtmlCodeFormatter(**formatter_opts) - return pygments.highlight(codeblock, lexer, formatter) - - def _code_block_sub(self, match, is_fenced_code_block=False): - lexer_name = None - if is_fenced_code_block: - lexer_name = match.group(1) - if lexer_name: - formatter_opts = self.extras['fenced-code-blocks'] or {} - codeblock = match.group(2) - codeblock = codeblock[:-1] # drop one trailing newline - else: - codeblock = match.group(1) - codeblock = self._outdent(codeblock) - codeblock = self._detab(codeblock) - codeblock = codeblock.lstrip('\n') # trim leading newlines - codeblock = codeblock.rstrip() # trim trailing whitespace - - # Note: "code-color" extra is DEPRECATED. - if "code-color" in self.extras and codeblock.startswith(":::"): - lexer_name, rest = codeblock.split('\n', 1) - lexer_name = lexer_name[3:].strip() - codeblock = rest.lstrip("\n") # Remove lexer declaration line. - formatter_opts = self.extras['code-color'] or {} - - if lexer_name: - def unhash_code(codeblock): - for key, sanitized in list(self.html_spans.items()): - codeblock = codeblock.replace(key, sanitized) - replacements = [ - ("&", "&"), - ("<", "<"), - (">", ">") - ] - for old, new in replacements: - codeblock = codeblock.replace(old, new) - return codeblock - lexer = self._get_pygments_lexer(lexer_name) - if lexer: - codeblock = unhash_code( codeblock ) - colored = self._color_with_pygments(codeblock, lexer, - **formatter_opts) - return "\n\n%s\n\n" % colored - - codeblock = self._encode_code(codeblock) - pre_class_str = self._html_class_str_from_tag("pre") - code_class_str = self._html_class_str_from_tag("code") - return "\n\n<pre%s><code%s>%s\n</code></pre>\n\n" % ( - pre_class_str, code_class_str, codeblock) - - def _html_class_str_from_tag(self, tag): - """Get the appropriate ' class="..."' string (note the leading - space), if any, for the given tag. - """ - if "html-classes" not in self.extras: - return "" - try: - html_classes_from_tag = self.extras["html-classes"] - except TypeError: - return "" - else: - if tag in html_classes_from_tag: - return ' class="%s"' % html_classes_from_tag[tag] - return "" - - def _do_code_blocks(self, text): - """Process Markdown `<pre><code>` blocks.""" - code_block_re = re.compile(r''' - (?:\n\n|\A\n?) - ( # $1 = the code block -- one or more lines, starting with a space/tab - (?: - (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces - .*\n+ - )+ - ) - ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc - # Lookahead to make sure this block isn't already in a code block. - # Needed when syntax highlighting is being used. - (?![^<]*\</code\>) - ''' % (self.tab_width, self.tab_width), - re.M | re.X) - return code_block_re.sub(self._code_block_sub, text) - - _fenced_code_block_re = re.compile(r''' - (?:\n+|\A\n?) - ^```([\w+-]+)?[ \t]*\n # opening fence, $1 = optional lang - (.*?) # $2 = code block content - ^```[ \t]*\n # closing fence - ''', re.M | re.X | re.S) - - def _fenced_code_block_sub(self, match): - return self._code_block_sub(match, is_fenced_code_block=True) - - def _do_fenced_code_blocks(self, text): - """Process ```-fenced unindented code blocks ('fenced-code-blocks' extra).""" - return self._fenced_code_block_re.sub(self._fenced_code_block_sub, text) - - # Rules for a code span: - # - backslash escapes are not interpreted in a code span - # - to include one or or a run of more backticks the delimiters must - # be a longer run of backticks - # - cannot start or end a code span with a backtick; pad with a - # space and that space will be removed in the emitted HTML - # See `test/tm-cases/escapes.text` for a number of edge-case - # examples. - _code_span_re = re.compile(r''' - (?<!\\) - (`+) # \1 = Opening run of ` - (?!`) # See Note A test/tm-cases/escapes.text - (.+?) # \2 = The code block - (?<!`) - \1 # Matching closer - (?!`) - ''', re.X | re.S) - - def _code_span_sub(self, match): - c = match.group(2).strip(" \t") - c = self._encode_code(c) - return "<code>%s</code>" % c - - def _do_code_spans(self, text): - # * Backtick quotes are used for <code></code> spans. - # - # * You can use multiple backticks as the delimiters if you want to - # include literal backticks in the code span. So, this input: - # - # Just type ``foo `bar` baz`` at the prompt. - # - # Will translate to: - # - # <p>Just type <code>foo `bar` baz</code> at the prompt.</p> - # - # There's no arbitrary limit to the number of backticks you - # can use as delimters. If you need three consecutive backticks - # in your code, use four for delimiters, etc. - # - # * You can use spaces to get literal backticks at the edges: - # - # ... type `` `bar` `` ... - # - # Turns to: - # - # ... type <code>`bar`</code> ... - return self._code_span_re.sub(self._code_span_sub, text) - - def _encode_code(self, text): - """Encode/escape certain characters inside Markdown code runs. - The point is that in code, these characters are literals, - and lose their special Markdown meanings. - """ - replacements = [ - # Encode all ampersands; HTML entities are not - # entities within a Markdown code span. - ('&', '&'), - # Do the angle bracket song and dance: - ('<', '<'), - ('>', '>'), - ] - for before, after in replacements: - text = text.replace(before, after) - hashed = _hash_text(text) - self._escape_table[text] = hashed - return hashed - - _strike_re = re.compile(r"~~(?=\S)(.+?)(?<=\S)~~", re.S) - def _do_strike(self, text): - text = self._strike_re.sub(r"<strike>\1</strike>", text) - return text - - _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S) - _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S) - _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S) - _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S) - def _do_italics_and_bold(self, text): - # <strong> must go first: - if "code-friendly" in self.extras: - text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text) - text = self._code_friendly_em_re.sub(r"<em>\1</em>", text) - else: - text = self._strong_re.sub(r"<strong>\2</strong>", text) - text = self._em_re.sub(r"<em>\2</em>", text) - return text - - # "smarty-pants" extra: Very liberal in interpreting a single prime as an - # apostrophe; e.g. ignores the fact that "round", "bout", "twer", and - # "twixt" can be written without an initial apostrophe. This is fine because - # using scare quotes (single quotation marks) is rare. - _apostrophe_year_re = re.compile(r"'(\d\d)(?=(\s|,|;|\.|\?|!|$))") - _contractions = ["tis", "twas", "twer", "neath", "o", "n", - "round", "bout", "twixt", "nuff", "fraid", "sup"] - def _do_smart_contractions(self, text): - text = self._apostrophe_year_re.sub(r"’\1", text) - for c in self._contractions: - text = text.replace("'%s" % c, "’%s" % c) - text = text.replace("'%s" % c.capitalize(), - "’%s" % c.capitalize()) - return text - - # Substitute double-quotes before single-quotes. - _opening_single_quote_re = re.compile(r"(?<!\S)'(?=\S)") - _opening_double_quote_re = re.compile(r'(?<!\S)"(?=\S)') - _closing_single_quote_re = re.compile(r"(?<=\S)'") - _closing_double_quote_re = re.compile(r'(?<=\S)"(?=(\s|,|;|\.|\?|!|$))') - def _do_smart_punctuation(self, text): - """Fancifies 'single quotes', "double quotes", and apostrophes. - Converts --, ---, and ... into en dashes, em dashes, and ellipses. - - Inspiration is: <http://daringfireball.net/projects/smartypants/> - See "test/tm-cases/smarty_pants.text" for a full discussion of the - support here and - <http://code.google.com/p/python-markdown2/issues/detail?id=42> for a - discussion of some diversion from the original SmartyPants. - """ - if "'" in text: # guard for perf - text = self._do_smart_contractions(text) - text = self._opening_single_quote_re.sub("‘", text) - text = self._closing_single_quote_re.sub("’", text) - - if '"' in text: # guard for perf - text = self._opening_double_quote_re.sub("“", text) - text = self._closing_double_quote_re.sub("”", text) - - text = text.replace("---", "—") - text = text.replace("--", "–") - text = text.replace("...", "…") - text = text.replace(" . . . ", "…") - text = text.replace(". . .", "…") - return text - - _block_quote_base = r''' - ( # Wrap whole match in \1 - ( - ^[ \t]*>%s[ \t]? # '>' at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - ''' - _block_quote_re = re.compile(_block_quote_base % '', re.M | re.X) - _block_quote_re_spoiler = re.compile(_block_quote_base % '[ \t]*?!?', re.M | re.X) - _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M) - _bq_one_level_re_spoiler = re.compile('^[ \t]*>[ \t]*?![ \t]?', re.M) - _bq_all_lines_spoilers = re.compile(r'\A(?:^[ \t]*>[ \t]*?!.*[\n\r]*)+\Z', re.M) - _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S) - def _dedent_two_spaces_sub(self, match): - return re.sub(r'(?m)^ ', '', match.group(1)) - - def _block_quote_sub(self, match): - bq = match.group(1) - is_spoiler = 'spoiler' in self.extras and self._bq_all_lines_spoilers.match(bq) - # trim one level of quoting - if is_spoiler: - bq = self._bq_one_level_re_spoiler.sub('', bq) - else: - bq = self._bq_one_level_re.sub('', bq) - # trim whitespace-only lines - bq = self._ws_only_line_re.sub('', bq) - bq = self._run_block_gamut(bq) # recurse - - bq = re.sub('(?m)^', ' ', bq) - # These leading spaces screw with <pre> content, so we need to fix that: - bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) - - if is_spoiler: - return '<blockquote class="spoiler">\n%s\n</blockquote>\n\n' % bq - else: - return '<blockquote>\n%s\n</blockquote>\n\n' % bq - - def _do_block_quotes(self, text): - if '>' not in text: - return text - if 'spoiler' in self.extras: - return self._block_quote_re_spoiler.sub(self._block_quote_sub, text) - else: - return self._block_quote_re.sub(self._block_quote_sub, text) - - def _form_paragraphs(self, text): - # Strip leading and trailing lines: - text = text.strip('\n') - - # Wrap <p> tags. - grafs = [] - for i, graf in enumerate(re.split(r"\n{2,}", text)): - if graf in self.html_blocks: - # Unhashify HTML blocks - grafs.append(self.html_blocks[graf]) - else: - cuddled_list = None - if "cuddled-lists" in self.extras: - # Need to put back trailing '\n' for `_list_item_re` - # match at the end of the paragraph. - li = self._list_item_re.search(graf + '\n') - # Two of the same list marker in this paragraph: a likely - # candidate for a list cuddled to preceding paragraph - # text (issue 33). Note the `[-1]` is a quick way to - # consider numeric bullets (e.g. "1." and "2.") to be - # equal. - if (li and len(li.group(2)) <= 3 and li.group("next_marker") - and li.group("marker")[-1] == li.group("next_marker")[-1]): - start = li.start() - cuddled_list = self._do_lists(graf[start:]).rstrip("\n") - assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>") - graf = graf[:start] - - # Wrap <p> tags. - graf = self._run_span_gamut(graf) - grafs.append("<p>" + graf.lstrip(" \t") + "</p>") - - if cuddled_list: - grafs.append(cuddled_list) - - return "\n\n".join(grafs) - - def _add_footnotes(self, text): - if self.footnotes: - footer = [ - '<div class="footnotes">', - '<hr' + self.empty_element_suffix, - '<ol>', - ] - for i, id in enumerate(self.footnote_ids): - if i != 0: - footer.append('') - footer.append('<li id="fn-%s">' % id) - footer.append(self._run_block_gamut(self.footnotes[id])) - backlink = ('<a href="#fnref-%s" ' - 'class="footnoteBackLink" ' - 'title="Jump back to footnote %d in the text.">' - '↩</a>' % (id, i+1)) - if footer[-1].endswith("</p>"): - footer[-1] = footer[-1][:-len("</p>")] \ - + ' ' + backlink + "</p>" - else: - footer.append("\n<p>%s</p>" % backlink) - footer.append('</li>') - footer.append('</ol>') - footer.append('</div>') - return text + '\n\n' + '\n'.join(footer) - else: - return text - - # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: - # http://bumppo.net/projects/amputator/ - _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)') - _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I) - _naked_gt_re = re.compile(r'''(?<![a-z0-9?!/'"-])>''', re.I) - - def _encode_amps_and_angles(self, text): - # Smart processing for ampersands and angle brackets that need - # to be encoded. - text = self._ampersand_re.sub('&', text) - - # Encode naked <'s - text = self._naked_lt_re.sub('<', text) - - # Encode naked >'s - # Note: Other markdown implementations (e.g. Markdown.pl, PHP - # Markdown) don't do this. - text = self._naked_gt_re.sub('>', text) - return text - - def _encode_backslash_escapes(self, text): - for ch, escape in list(self._escape_table.items()): - text = text.replace("\\"+ch, escape) - return text - - _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I) - def _auto_link_sub(self, match): - g1 = match.group(1) - return '<a href="%s">%s</a>' % (g1, g1) - - _auto_email_link_re = re.compile(r""" - < - (?:mailto:)? - ( - [-.\w]+ - \@ - [-\w]+(\.[-\w]+)*\.[a-z]+ - ) - > - """, re.I | re.X | re.U) - def _auto_email_link_sub(self, match): - return self._encode_email_address( - self._unescape_special_chars(match.group(1))) - - def _do_auto_links(self, text): - text = self._auto_link_re.sub(self._auto_link_sub, text) - text = self._auto_email_link_re.sub(self._auto_email_link_sub, text) - return text - - def _encode_email_address(self, addr): - # Input: an email address, e.g. "foo@example.com" - # - # Output: the email address as a mailto link, with each character - # of the address encoded as either a decimal or hex entity, in - # the hopes of foiling most address harvesting spam bots. E.g.: - # - # <a href="mailto:foo@e - # xample.com">foo - # @example.com</a> - # - # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk - # mailing list: <http://tinyurl.com/yu7ue> - chars = [_xml_encode_email_char_at_random(ch) - for ch in "mailto:" + addr] - # Strip the mailto: from the visible part. - addr = '<a href="%s">%s</a>' \ - % (''.join(chars), ''.join(chars[7:])) - return addr - - def _do_link_patterns(self, text): - """Caveat emptor: there isn't much guarding against link - patterns being formed inside other standard Markdown links, e.g. - inside a [link def][like this]. - - Dev Notes: *Could* consider prefixing regexes with a negative - lookbehind assertion to attempt to guard against this. - """ - link_from_hash = {} - for regex, repl in self.link_patterns: - replacements = [] - for match in regex.finditer(text): - if hasattr(repl, "__call__"): - href = repl(match) - else: - href = match.expand(repl) - replacements.append((match.span(), href)) - for (start, end), href in reversed(replacements): - escaped_href = ( - href.replace('"', '"') # b/c of attr quote - # To avoid markdown <em> and <strong>: - .replace('*', self._escape_table['*']) - .replace('_', self._escape_table['_'])) - link = '<a href="%s">%s</a>' % (escaped_href, text[start:end]) - hash = _hash_text(link) - link_from_hash[hash] = link - text = text[:start] + hash + text[end:] - for hash, link in list(link_from_hash.items()): - text = text.replace(hash, link) - return text - - def _unescape_special_chars(self, text): - # Swap back in all the special characters we've hidden. - for ch, hash in list(self._escape_table.items()): - text = text.replace(hash, ch) - return text - - def _outdent(self, text): - # Remove one level of line-leading tabs or spaces - return self._outdent_re.sub('', text) - - -class MarkdownWithExtras(Markdown): - """A markdowner class that enables most extras: - - - footnotes - - code-color (only has effect if 'pygments' Python module on path) - - These are not included: - - pyshell (specific to Python-related documenting) - - code-friendly (because it *disables* part of the syntax) - - link-patterns (because you need to specify some actual - link-patterns anyway) - """ - extras = ["footnotes", "code-color"] - - -# ---- internal support functions - -class UnicodeWithAttrs(unicode): - """A subclass of unicode used for the return value of conversion to - possibly attach some attributes. E.g. the "toc_html" attribute when - the "toc" extra is used. - """ - metadata = None - _toc = None - def toc_html(self): - """Return the HTML for the current TOC. - - This expects the `_toc` attribute to have been set on this instance. - """ - if self._toc is None: - return None - - def indent(): - return ' ' * (len(h_stack) - 1) - lines = [] - h_stack = [0] # stack of header-level numbers - for level, id, name in self._toc: - if level > h_stack[-1]: - lines.append("%s<ul>" % indent()) - h_stack.append(level) - elif level == h_stack[-1]: - lines[-1] += "</li>" - else: - while level < h_stack[-1]: - h_stack.pop() - if not lines[-1].endswith("</li>"): - lines[-1] += "</li>" - lines.append("%s</ul></li>" % indent()) - lines.append('%s<li><a href="#%s">%s</a>' % ( - indent(), id, name)) - while len(h_stack) > 1: - h_stack.pop() - if not lines[-1].endswith("</li>"): - lines[-1] += "</li>" - lines.append("%s</ul>" % indent()) - return '\n'.join(lines) + '\n' - toc_html = property(toc_html) - -## {{{ http://code.activestate.com/recipes/577257/ (r1) -_slugify_strip_re = re.compile(r'[^\w\s-]') -_slugify_hyphenate_re = re.compile(r'[-\s]+') -def _slugify(value): - """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. - - From Django's "django/template/defaultfilters.py". - """ - import unicodedata - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode() - value = _slugify_strip_re.sub('', value).strip().lower() - return _slugify_hyphenate_re.sub('-', value) -## end of http://code.activestate.com/recipes/577257/ }}} - - -# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549 -def _curry(*args, **kwargs): - function, args = args[0], args[1:] - def result(*rest, **kwrest): - combined = kwargs.copy() - combined.update(kwrest) - return function(*args + rest, **combined) - return result - - -# Recipe: regex_from_encoded_pattern (1.0) -def _regex_from_encoded_pattern(s): - """'foo' -> re.compile(re.escape('foo')) - '/foo/' -> re.compile('foo') - '/foo/i' -> re.compile('foo', re.I) - """ - if s.startswith('/') and s.rfind('/') != 0: - # Parse it: /PATTERN/FLAGS - idx = s.rfind('/') - pattern, flags_str = s[1:idx], s[idx+1:] - flag_from_char = { - "i": re.IGNORECASE, - "l": re.LOCALE, - "s": re.DOTALL, - "m": re.MULTILINE, - "u": re.UNICODE, - } - flags = 0 - for char in flags_str: - try: - flags |= flag_from_char[char] - except KeyError: - raise ValueError("unsupported regex flag: '%s' in '%s' " - "(must be one of '%s')" - % (char, s, ''.join(list(flag_from_char.keys())))) - return re.compile(s[1:idx], flags) - else: # not an encoded regex - return re.compile(re.escape(s)) - - -# Recipe: dedent (0.1.2) -def _dedentlines(lines, tabsize=8, skip_first_line=False): - """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines - - "lines" is a list of lines to dedent. - "tabsize" is the tab width to use for indent width calculations. - "skip_first_line" is a boolean indicating if the first line should - be skipped for calculating the indent width and for dedenting. - This is sometimes useful for docstrings and similar. - - Same as dedent() except operates on a sequence of lines. Note: the - lines list is modified **in-place**. - """ - DEBUG = False - if DEBUG: - print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ - % (tabsize, skip_first_line)) - margin = None - for i, line in enumerate(lines): - if i == 0 and skip_first_line: continue - indent = 0 - for ch in line: - if ch == ' ': - indent += 1 - elif ch == '\t': - indent += tabsize - (indent % tabsize) - elif ch in '\r\n': - continue # skip all-whitespace lines - else: - break - else: - continue # skip all-whitespace lines - if DEBUG: print("dedent: indent=%d: %r" % (indent, line)) - if margin is None: - margin = indent - else: - margin = min(margin, indent) - if DEBUG: print("dedent: margin=%r" % margin) - - if margin is not None and margin > 0: - for i, line in enumerate(lines): - if i == 0 and skip_first_line: continue - removed = 0 - for j, ch in enumerate(line): - if ch == ' ': - removed += 1 - elif ch == '\t': - removed += tabsize - (removed % tabsize) - elif ch in '\r\n': - if DEBUG: print("dedent: %r: EOL -> strip up to EOL" % line) - lines[i] = lines[i][j:] - break - else: - raise ValueError("unexpected non-whitespace char %r in " - "line %r while removing %d-space margin" - % (ch, line, margin)) - if DEBUG: - print("dedent: %r: %r -> removed %d/%d"\ - % (line, ch, removed, margin)) - if removed == margin: - lines[i] = lines[i][j+1:] - break - elif removed > margin: - lines[i] = ' '*(removed-margin) + lines[i][j+1:] - break - else: - if removed: - lines[i] = lines[i][removed:] - return lines - - -def _dedent(text, tabsize=8, skip_first_line=False): - """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text - - "text" is the text to dedent. - "tabsize" is the tab width to use for indent width calculations. - "skip_first_line" is a boolean indicating if the first line should - be skipped for calculating the indent width and for dedenting. - This is sometimes useful for docstrings and similar. - - textwrap.dedent(s), but don't expand tabs to spaces - """ - lines = text.splitlines(1) - _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line) - return ''.join(lines) - - -class _memoized(object): - """Decorator that caches a function's return value each time it is called. - If called later with the same arguments, the cached value is returned, and - not re-evaluated. - - http://wiki.python.org/moin/PythonDecoratorLibrary - """ - def __init__(self, func): - self.func = func - self.cache = {} - - def __call__(self, *args): - try: - return self.cache[args] - except KeyError: - self.cache[args] = value = self.func(*args) - return value - except TypeError: - # uncachable -- for instance, passing a list as an argument. - # Better to not cache than to blow up entirely. - return self.func(*args) - - def __repr__(self): - """Return the function's docstring.""" - return self.func.__doc__ - - -def _xml_oneliner_re_from_tab_width(tab_width): - """Standalone XML processing instruction regex.""" - return re.compile(r""" - (?: - (?<=\n\n) # Starting after a blank line - | # or - \A\n? # the beginning of the doc - ) - ( # save in $1 - [ ]{0,%d} - (?: - <\?\w+\b\s+.*?\?> # XML processing instruction - | - <\w+:\w+\b\s+.*?/> # namespaced single tag - ) - [ \t]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - ) - """ % (tab_width - 1), re.X) -_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width) - - -def _hr_tag_re_from_tab_width(tab_width): - return re.compile(r""" - (?: - (?<=\n\n) # Starting after a blank line - | # or - \A\n? # the beginning of the doc - ) - ( # save in \1 - [ ]{0,%d} - <(hr) # start tag = \2 - \b # word break - ([^<>])*? # - /?> # the matching end tag - [ \t]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - ) - """ % (tab_width - 1), re.X) -_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width) - - -def _xml_escape_attr(attr, skip_single_quote=True): - """Escape the given string for use in an HTML/XML tag attribute. - - By default this doesn't bother with escaping `'` to `'`, presuming that - the tag attribute is surrounded by double quotes. - """ - escaped = (attr - .replace('&', '&') - .replace('"', '"') - .replace('<', '<') - .replace('>', '>')) - if not skip_single_quote: - escaped = escaped.replace("'", "'") - return escaped - - -def _xml_encode_email_char_at_random(ch): - r = random() - # Roughly 10% raw, 45% hex, 45% dec. - # '@' *must* be encoded. I [John Gruber] insist. - # Issue 26: '_' must be encoded. - if r > 0.9 and ch not in "@_": - return ch - elif r < 0.45: - # The [1:] is to drop leading '0': 0x63 -> x63 - return '&#%s;' % hex(ord(ch))[1:] - else: - return '&#%s;' % ord(ch) - - -# ---- mainline - -class _NoReflowFormatter(optparse.IndentedHelpFormatter): - """An optparse formatter that does NOT reflow the description.""" - def format_description(self, description): - return description or "" - - -def _test(): - import doctest - doctest.testmod() - - -def main(argv=None): - if argv is None: - argv = sys.argv - if not logging.root.handlers: - logging.basicConfig() - - usage = "usage: %prog [PATHS...]" - version = "%prog "+__version__ - parser = optparse.OptionParser(prog="markdown2", usage=usage, - version=version, description=cmdln_desc, - formatter=_NoReflowFormatter()) - parser.add_option("-v", "--verbose", dest="log_level", - action="store_const", const=logging.DEBUG, - help="more verbose output") - parser.add_option("--encoding", - help="specify encoding of text content") - parser.add_option("--html4tags", action="store_true", default=False, - help="use HTML 4 style for empty element tags") - parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode", - help="sanitize literal HTML: 'escape' escapes " - "HTML meta chars, 'replace' replaces with an " - "[HTML_REMOVED] note") - parser.add_option("-x", "--extras", action="append", - help="Turn on specific extra features (not part of " - "the core Markdown spec). See above.") - parser.add_option("--use-file-vars", - help="Look for and use Emacs-style 'markdown-extras' " - "file var to turn on extras. See " - "<https://github.com/trentm/python-markdown2/wiki/Extras>") - parser.add_option("--link-patterns-file", - help="path to a link pattern file") - parser.add_option("--self-test", action="store_true", - help="run internal self-tests (some doctests)") - parser.add_option("--compare", action="store_true", - help="run against Markdown.pl as well (for testing)") - parser.set_defaults(log_level=logging.INFO, compare=False, - encoding="utf-8", safe_mode=None, use_file_vars=False) - opts, paths = parser.parse_args() - log.setLevel(opts.log_level) - - if opts.self_test: - return _test() - - if opts.extras: - extras = {} - for s in opts.extras: - splitter = re.compile("[,;: ]+") - for e in splitter.split(s): - if '=' in e: - ename, earg = e.split('=', 1) - try: - earg = int(earg) - except ValueError: - pass - else: - ename, earg = e, None - extras[ename] = earg - else: - extras = None - - if opts.link_patterns_file: - link_patterns = [] - f = open(opts.link_patterns_file) - try: - for i, line in enumerate(f.readlines()): - if not line.strip(): continue - if line.lstrip().startswith("#"): continue - try: - pat, href = line.rstrip().rsplit(None, 1) - except ValueError: - raise MarkdownError("%s:%d: invalid link pattern line: %r" - % (opts.link_patterns_file, i+1, line)) - link_patterns.append( - (_regex_from_encoded_pattern(pat), href)) - finally: - f.close() - else: - link_patterns = None - - from os.path import join, dirname, abspath, exists - markdown_pl = join(dirname(dirname(abspath(__file__))), "test", - "Markdown.pl") - if not paths: - paths = ['-'] - for path in paths: - if path == '-': - text = sys.stdin.read() - else: - fp = codecs.open(path, 'r', opts.encoding) - text = fp.read() - fp.close() - if opts.compare: - from subprocess import Popen, PIPE - print("==== Markdown.pl ====") - p = Popen('perl %s' % markdown_pl, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True) - p.stdin.write(text.encode('utf-8')) - p.stdin.close() - perl_html = p.stdout.read().decode('utf-8') - if py3: - sys.stdout.write(perl_html) - else: - sys.stdout.write(perl_html.encode( - sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) - print("==== markdown2.py ====") - html = markdown(text, - html4tags=opts.html4tags, - safe_mode=opts.safe_mode, - extras=extras, link_patterns=link_patterns, - use_file_vars=opts.use_file_vars) - if py3: - sys.stdout.write(html) - else: - sys.stdout.write(html.encode( - sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) - if extras and "toc" in extras: - log.debug("toc_html: " + - str(html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))) - if opts.compare: - test_dir = join(dirname(dirname(abspath(__file__))), "test") - if exists(join(test_dir, "test_markdown2.py")): - sys.path.insert(0, test_dir) - from test_markdown2 import norm_html_from_html - norm_html = norm_html_from_html(html) - norm_perl_html = norm_html_from_html(perl_html) - else: - norm_html = html - norm_perl_html = perl_html - print("==== match? %r ====" % (norm_perl_html == norm_html)) - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/addons21/fastwq/libs/AnkiHub/updates.py b/addons21/fastwq/libs/AnkiHub/updates.py deleted file mode 100644 index 38e7d8c..0000000 --- a/addons21/fastwq/libs/AnkiHub/updates.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- - -from aqt.qt import * - -try: - _encoding = QApplication.UnicodeUTF8 - def _translate(context, text, disambig): - return QApplication.translate(context, text, disambig, _encoding) -except AttributeError: - def _translate(context, text, disambig): - return QApplication.translate(context, text, disambig) - -class Ui_DialogUpdates(object): - def setupUi(self, DialogUpdates): - DialogUpdates.setObjectName(u"DialogUpdates") - DialogUpdates.resize(500, 400) - self.verticalLayout = QVBoxLayout(DialogUpdates) - self.verticalLayout.setObjectName(u"verticalLayout") - self.labelUpdates = QLabel(DialogUpdates) - self.labelUpdates.setWordWrap(True) - self.labelUpdates.setOpenExternalLinks(True) - self.labelUpdates.setObjectName(u"labelUpdates") - self.verticalLayout.addWidget(self.labelUpdates) - self.textBrowser = QTextBrowser(DialogUpdates) - self.textBrowser.setObjectName(u"textBrowser") - self.verticalLayout.addWidget(self.textBrowser) - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.update = QPushButton(DialogUpdates) - self.update.setObjectName(u"update") - self.horizontalLayout.addWidget(self.update, 0, Qt.AlignCenter) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(DialogUpdates) - QMetaObject.connectSlotsByName(DialogUpdates) - - def retranslateUi(self, DialogUpdates): - DialogUpdates.setWindowTitle(_translate("DialogUpdates", "FastWQ - Updater", None)) - self.labelUpdates.setText(_translate( - "DialogUpdates", - "<html><head/><body>\ - <p>A new version of {0} is available for download! </p>\ - <p>Do you want to update {1}to version {2}?</p>\ - <p>Changes from your version are listed below:</p>\ - </body></html>", - None - )) - self.update.setText(_translate("DialogUpdates", "Update", None)) diff --git a/addons21/fastwq/libs/ankihub.py b/addons21/fastwq/libs/ankihub.py deleted file mode 100644 index b009b84..0000000 --- a/addons21/fastwq/libs/ankihub.py +++ /dev/null @@ -1,311 +0,0 @@ -try: - import httplib -except: - import http.client as httplib -try: - import urllib2 -except: - import urllib.request as urllib2 -import json -import os -import sys -import zipfile -import traceback -import io -import aqt -from aqt import mw -from aqt.qt import * -from aqt.utils import showInfo -from anki.hooks import addHook -from anki.utils import isMac, isWin -from ..context import APP_ICON -from .AnkiHub.updates import Ui_DialogUpdates -from .AnkiHub.markdown2 import markdown - - -__all__ = ['update'] - - -# taken from Anki's aqt/profiles.py -def defaultBase(): - path = mw.pm.addonFolder() - return os.path.dirname(os.path.abspath(path)) - - -headers = {"User-Agent": "AnkiHub"} -dataPath = os.path.join(defaultBase(),'.fastwq_2.1.x_ankihub.json') - - -class DialogUpdates(QDialog, Ui_DialogUpdates): - - def __init__(self, parent, data, oldData, callback): - parent = parent if parent else mw - QDialog.__init__(self, parent) - self.setModal(True) - self.setWindowFlags( - self.windowFlags() & - ~Qt.WindowContextHelpButtonHint - ) - self.setWindowIcon(APP_ICON) - self.setupUi(self) - totalSize = sum(map(lambda x:x['size'],data['assets'])) - def answer(): - self.update.setEnabled(False) - callback(self.appendHtml, self.finish) - - self.html = u'' - self.appendHtml(markdown(data['body'])) - - #if not automaticAnswer: - self.update.clicked.connect(lambda:answer()) - - fromVersion = '' - if 'tag_name' in oldData: - fromVersion = u'from {0} '.format(oldData['tag_name']) - self.labelUpdates.setText( - str(self.labelUpdates.text()).format( - data['name'], - fromVersion, - data['tag_name'])) - - def appendHtml(self,html='',temp=''): - self.html += html - self.textBrowser.setHtml(u'<html><body>{0}{1}{2}</body></html>'.format(self.html, temp, u'<div id="text_bottom"></div>')) - self.textBrowser.scrollToAnchor('text_bottom') - - def finish(self): - self.hide() - self.destroy() - showInfo('Updated. Please restart Anki.') - pass - - -def installZipFile(data, fname): - #base = os.path.join(mw.pm.addonFolder(), 'fastwq') - base = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../')) - if fname.endswith(".py"): - path = os.path.join(base, fname) - with open(path, "wb") as file: - file.write(data) - file.close() - return True - # .zip file - try: - z = zipfile.ZipFile(io.BytesIO(data)) - except zipfile.BadZipfile: - return False - for n in z.namelist(): - if n.endswith("/"): - # folder; ignore - continue - # write - try: - z.extract(n, base) - except: - print(n) - return True - - -def asset(a): - return { - 'url': a['browser_download_url'], - 'size': a['size'] - } - - -def updateSingle(repositories, path, data): - def callback(appendHtml, onReady): - for asset in data['assets']: - code = asset['url'] - p, fname = os.path.split(code) - appendHtml(temp='<br />Downloading {1}: {0}%<br/>'.format(0,fname)) - try: - urlthread = UrlThread(code) - urlthread.start() - urlthread.join() - response = urlthread.response#urllib2.urlopen(code) - meta = response.info() - file_size = int(meta.get("Content-Length")) - except: - appendHtml('Downloading file error!<br/>') - return - d = b'' - dl = 0 - i = 0 - lastPercent = None - while True: - dkb = response.read(1024) - if not dkb: - break - dl += len(dkb) - d += dkb - if dl*100/file_size>i: - lastPercent = int(dl*100/file_size) - i = lastPercent+1 - appendHtml(temp='<br />Downloading {1}: {0}%<br/>'.format(lastPercent,fname)) - QApplication.instance().processEvents() - appendHtml('<br />Downloading {1}: 100%<br/>'.format(int(dl*100/file_size),fname)) - appendHtml('Installing ...<br/>') - if not installZipFile(d, fname): - appendHtml('Corrupt file<br/>') - else: - repositories[path] = data - repositories[path]['update'] = 'ask' - with open(dataPath,'w') as f: - json.dump(repositories,f,indent=2) - f.close() - appendHtml('Done.<br/>') - onReady() # close the AnkiHub update window - - return callback - - -def update(add=[], VERSION='v0.0.0', background=False, parent=None): - parent = parent if parent else mw - # progress win - if not background: - progresswin = QProgressDialog('Update Checking...', '', 0, 0, parent) - progresswin.setWindowModality(Qt.ApplicationModal) - progresswin.setCancelButton(None) - progresswin.setWindowFlags( - progresswin.windowFlags() & - ~Qt.WindowContextHelpButtonHint - ) - progresswin.setWindowTitle('FastWQ - Updater') - progresswin.setWindowIcon(APP_ICON) - progresswin.resize(280, 60) - progresswin.show() - else: - progresswin = None - # - conn = httplib.HTTPSConnection("api.github.com") - try: - with open(dataPath,'r') as f: - repositories = json.load(f) - f.close() - except: - repositories = {} - - for a in add: - if a not in repositories: - repositories[a] = { - 'id': 0, - 'update': 'ask' - } - - for path,repository in repositories.items(): - username,repositoryName = path.split('/') - try: - urlthread = UrlThread("https://api.github.com/repos/{0}/releases/latest".format(path)) - urlthread.start() - urlthread.join() - release = json.loads(urlthread.response.read()) - except Exception as e: - release = {} - - if 'id' in release: - if release['id'] != repository['id']: - data = { - 'id': release['id'], - 'name': repositoryName, - 'tag_name': release['tag_name'], - 'body': '### {0}\n'.format(release['name']) + release['body'], - 'assets': [asset(release['assets'][1])], - 'update': 'ask' - } - if 'tag_name' in repository: - oldVersion = map(int,repository['tag_name'][1:].split('.')) - oldVersion = [x for x in oldVersion] - while len(oldVersion)<3: - oldVersion.append(0) - else: - oldVersion = map(int,VERSION[1:].split('.'))#[0,0,0] - oldVersion = [x for x in oldVersion] - newVersion = map(int,data['tag_name'][1:].split('.')) - newVersion = [x for x in newVersion] - isMinor = len(newVersion)>2 and newVersion[2]>0 - while len(newVersion)<3: - newVersion.append(0) - i = oldVersion[2]+1 - if oldVersion[0]<newVersion[0] or oldVersion[1]<newVersion[1]: - if isMinor: - i = 1 - while i<newVersion[2]: - if progresswin and progresswin.wasCanceled(): - break - try: - minorTagName = 'v{0}.{1}.{2}'.format(newVersion[0],oldVersion[1],i) - urlthread = UrlThread("https://api.github.com/repos/{0}/releases/tags/{1}".format(path,minorTagName)) - urlthread.start() - urlthread.join() - responseData = urlthread.response.read() - minor = json.loads(responseData) - data['body'] += '\n\n### {0}\n'.format(minor['name']) + minor['body'] - except: - pass - i += 1 - if oldVersion[0]<newVersion[0] or oldVersion[1]<newVersion[1]: - # new major release necessary! - # if the newest version is minor, fetch the additional assets from the major - if isMinor and (background or not progresswin.wasCanceled()): - try: - majorTagName = 'v{0}.{1}'.format(newVersion[0],newVersion[1]) - urlthread = UrlThread( - "https://api.github.com/repos/{0}/releases/tags/{1}".format(path,majorTagName), - "https://api.github.com/repos/{0}/releases/tags/{1}.0".format(path,majorTagName) - ) - urlthread.start() - urlthread.join() - responseData = urlthread.response.read() - major = json.loads(responseData) - data['body'] += '\n\n### {0}\n'.format(major['name']) + major['body'] - except: - pass - - if background or not progresswin.wasCanceled(): - if progresswin: - progresswin.hide() - progresswin.destroy() - dialog = DialogUpdates(parent, data, repository, updateSingle(repositories, path, data)) - dialog.exec_() - dialog.destroy() - else: - if progresswin: - progresswin.hide() - progresswin.destroy() - return 1 - else: - if progresswin: - progresswin.hide() - progresswin.destroy() - return 0 - if progresswin: - progresswin.hide() - progresswin.destroy() - return -1 - - -class UrlThread(QThread): - - def __init__(self, url, backurl=None): - super(UrlThread, self).__init__() - self.response = None - self.url = url - self.backurl = backurl - self.finished = False - - def run(self): - try: - self.response = urllib2.urlopen(self.url) - except: - if self.backurl: - try: - self.response = urllib2.urlopen(self.backurl) - except: - pass - self.finished = True - - def join(self): - while not self.finished: - QApplication.instance().processEvents() - self.wait(30) From ab9593074b367a936f474603e313c2d1d173855b Mon Sep 17 00:00:00 2001 From: "St.Huang" <wenhonghuang@gmail.com> Date: Sun, 10 Feb 2019 11:44:25 +0800 Subject: [PATCH 3/6] code format --- addons21/fastwq/__init__.py | 9 +- addons21/fastwq/common.py | 110 +++++----- addons21/fastwq/constants.py | 24 ++- addons21/fastwq/context.py | 23 ++- addons21/fastwq/gui/__init__.py | 2 +- addons21/fastwq/gui/base.py | 16 +- addons21/fastwq/gui/common.py | 2 +- addons21/fastwq/gui/dictmanager.py | 37 ++-- addons21/fastwq/gui/foldermanager.py | 21 +- addons21/fastwq/gui/options.py | 291 ++++++++++++++------------- addons21/fastwq/gui/progress.py | 29 +-- addons21/fastwq/gui/setting.py | 14 +- addons21/fastwq/lang.py | 35 ++-- 13 files changed, 330 insertions(+), 283 deletions(-) diff --git a/addons21/fastwq/__init__.py b/addons21/fastwq/__init__.py index 1e3504c..e02a78b 100644 --- a/addons21/fastwq/__init__.py +++ b/addons21/fastwq/__init__.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -19,6 +19,7 @@ import ssl import sys + from anki.hooks import addHook from anki.utils import isMac @@ -28,6 +29,7 @@ if isMac: ############## other config here ################## shortcut = ('Ctrl+Alt' if isMac else 'Ctrl') + '+Q' + ################################################### @@ -42,7 +44,6 @@ def start_here(): fastwq.browser_menu() fastwq.context_menu() fastwq.customize_addcards() - # if fastwq.config.auto_update: - # fastwq.check_updates(True) -addHook("profileLoaded", start_here) \ No newline at end of file + +addHook("profileLoaded", start_here) diff --git a/addons21/fastwq/common.py b/addons21/fastwq/common.py index 956cfd5..94f0e3a 100644 --- a/addons21/fastwq/common.py +++ b/addons21/fastwq/common.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -18,24 +18,23 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from operator import itemgetter + +from anki.hooks import addHook, remHook, wrap from aqt import mw -from aqt.qt import * -from anki.hooks import addHook, wrap, remHook from aqt.addcards import AddCards -from aqt.utils import showInfo, shortcut, downArrow -from .gui import show_options, show_about_dialog #, check_updates -from .query import query_from_browser, query_from_editor_fields -from .context import config, APP_ICON, Config +from aqt.qt import * +from aqt.utils import downArrow, shortcut, showInfo + +from .context import APP_ICON, Config, config +from .gui import show_about_dialog, show_options # , check_updates from .lang import _ +from .query import query_from_browser, query_from_editor_fields from .service import service_pool from .utils import get_icon - __all__ = [ - 'browser_menu', 'customize_addcards', - 'config_menu', 'context_menu' -] # 'check_updates', - + 'browser_menu', 'customize_addcards', 'config_menu', 'context_menu' +] # 'check_updates', have_setup = False my_shortcut = '' @@ -52,18 +51,23 @@ def set_options_def(mid, i): data = dict() data[mid] = conf config.update(data) + + # end set_options_def + def browser_menu(): """ add add-on's menu to browser window """ + def on_setup_menus(browser): """ on browser setupMenus was called """ # main menu menu = browser.form.menubar.addMenu("FastWQ") + # menu gen def init_fastwq_menu(): try: @@ -78,6 +82,7 @@ def browser_menu(): menu.addAction(action) # Options action = QAction(_('OPTIONS'), browser) + def _show_options(): model_id = -1 for note_id in browser.selectedNotes(): @@ -85,26 +90,31 @@ def browser_menu(): model_id = note.model()['id'] break show_options(browser, model_id) + action.triggered.connect(_show_options) menu.addAction(action) # Default configs menu.addSeparator() b = False - for m in sorted(browser.mw.col.models.all(), key=itemgetter("name")): + for m in sorted( + browser.mw.col.models.all(), key=itemgetter("name")): conf = config.get_maps(m['id']) - conf = {'list': [conf], 'def': 0} if isinstance(conf, list) else conf + conf = { + 'list': [conf], + 'def': 0 + } if isinstance(conf, list) else conf maps_list = conf['list'] if len(maps_list) > 1: submenu = menu.addMenu(m['name']) for i, maps in enumerate(maps_list): submenu.addAction( - _OK_ICON if i==conf['def'] else _NULL_ICON, - _('CONFIG_INDEX') % (i+1) if isinstance(maps, list) else maps['name'], - lambda mid=m['id'], i=i: set_options_def(mid, i) - ) + _OK_ICON if i == conf['def'] else _NULL_ICON, + _('CONFIG_INDEX') % (i + 1) if isinstance( + maps, list) else maps['name'], + lambda mid=m['id'], i=i: set_options_def(mid, i)) b = True - if b: + if b: menu.addSeparator() # # check update @@ -120,6 +130,7 @@ def browser_menu(): # end init_fastwq_menu init_fastwq_menu() addHook('config.update', init_fastwq_menu) + addHook('browser.setupMenus', on_setup_menus) @@ -127,6 +138,7 @@ def customize_addcards(): """ add button to addcards window """ + def add_query_button(self): ''' add a button in add card window @@ -136,44 +148,47 @@ def customize_addcards(): # button fastwqBtn = QPushButton(_("QUERY") + u" " + downArrow()) fastwqBtn.setShortcut(QKeySequence(my_shortcut)) - fastwqBtn.setToolTip( - _(u"Shortcut: %s") % shortcut(my_shortcut) - ) + fastwqBtn.setToolTip(_(u"Shortcut: %s") % shortcut(my_shortcut)) bb.addButton(fastwqBtn, ar) + # signal def onQuery(e): if isinstance(e, QMouseEvent): if e.buttons() & Qt.LeftButton: menu = QMenu(self) - menu.addAction(_("ALL_FIELDS"), lambda: query_from_editor_fields(self.editor), QKeySequence(my_shortcut)) + menu.addAction( + _("ALL_FIELDS"), + lambda: query_from_editor_fields(self.editor), + QKeySequence(my_shortcut)) # default options mid = self.editor.note.model()['id'] conf = config.get_maps(mid) - conf = {'list': [conf], 'def': 0} if isinstance(conf, list) else conf + conf = { + 'list': [conf], + 'def': 0 + } if isinstance(conf, list) else conf maps_list = conf['list'] if len(maps_list) > 1: menu.addSeparator() for i, maps in enumerate(maps_list): menu.addAction( - _OK_ICON if i==conf['def'] else _NULL_ICON, - _('CONFIG_INDEX') % (i+1) if isinstance(maps, list) else maps['name'], - lambda mid=mid, i=i: set_options_def(mid, i) - ) + _OK_ICON if i == conf['def'] else _NULL_ICON, + _('CONFIG_INDEX') % (i + 1) if isinstance( + maps, list) else maps['name'], + lambda mid=mid, i=i: set_options_def(mid, i)) menu.addSeparator() # end default options menu.addAction(_("OPTIONS"), lambda: show_options(self, self.editor.note.model()['id'])) - menu.exec_(fastwqBtn.mapToGlobal(QPoint(0, fastwqBtn.height()))) + menu.exec_( + fastwqBtn.mapToGlobal(QPoint(0, fastwqBtn.height()))) else: query_from_editor_fields(self.editor) fastwqBtn.mousePressEvent = onQuery fastwqBtn.clicked.connect(onQuery) - - AddCards.setupButtons = wrap( - AddCards.setupButtons, - add_query_button, - "after" - ) + + AddCards.setupButtons = wrap(AddCards.setupButtons, add_query_button, + "after") def config_menu(): @@ -187,6 +202,7 @@ def config_menu(): def context_menu(): '''mouse right click menu''' + def on_setup_menus(web_view, menu): """ add context menu to webview @@ -217,32 +233,30 @@ def context_menu(): name = s.title + ' :-> ' + s.fields[dict_fld_ord] if name not in names: names.append(name) - curr_flds.append({ - 'name': name, - 'def': i - }) + curr_flds.append({'name': name, 'def': i}) service_pool.put(s) submenu = menu.addMenu(_('QUERY')) - submenu.addAction(_('ALL_FIELDS'), lambda: query_from_editor_fields(web_view.editor), QKeySequence(my_shortcut)) + submenu.addAction( + _('ALL_FIELDS'), lambda: query_from_editor_fields(web_view.editor), + QKeySequence(my_shortcut)) if len(curr_flds) > 0: # quer hook method def query_from_editor_hook(i): conf = config.get_maps(current_model_id) - maps_old_def = 0 if isinstance(conf, list) else conf.get('def', 0) + maps_old_def = 0 if isinstance(conf, list) else conf.get( + 'def', 0) set_options_def(current_model_id, i) query_from_editor_fields( - web_view.editor, - fields=[web_view.editor.currentField] - ) + web_view.editor, fields=[web_view.editor.currentField]) set_options_def(current_model_id, maps_old_def) + # sub menu - #flds_menu = submenu.addMenu(_('CURRENT_FIELDS')) + # flds_menu = submenu.addMenu(_('CURRENT_FIELDS')) submenu.addSeparator() for c in curr_flds: - submenu.addAction(c['name'], - lambda i=c['def']: query_from_editor_hook(i) - ) + submenu.addAction( + c['name'], lambda i=c['def']: query_from_editor_hook(i)) submenu.addSeparator() submenu.addAction(_("OPTIONS"), lambda: show_options(web_view, web_view.editor.note.model()['id'])) diff --git a/addons21/fastwq/constants.py b/addons21/fastwq/constants.py index 58de6fe..86728c9 100644 --- a/addons21/fastwq/constants.py +++ b/addons21/fastwq/constants.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - from .lang import _ - __all__ = ['VERSION', 'Endpoint', 'Template'] -VERSION = 'v1.3.5' +VERSION = 'v2.0.0b' + class Endpoint: repository = u'https://github.com/sth2018/FastWordQuery' @@ -36,10 +35,13 @@ class Endpoint: class Template: tmpl_about = u'''<b>{t0}</b><br/>{version}<br/><b>{t1}</b><br/> - <a href="{url}">{url}</a><br/><b>{t2}</b><br/> - <a href="{feedback0}">{feedback0}</a><br/> - <a href="mailto:{feedback1}">{feedback1}</a>'''.format( - t0=_('VERSION'), version=VERSION, - t1=_('REPOSITORY'), url=Endpoint.repository, - t2=_('FEEDBACK'), feedback0=Endpoint.feedback_issue, feedback1=Endpoint.feedback_mail) - miss_css = u'MDX dictonary <b>{dict}</b> misses css file <b>{css}</b>. <br/>Please choose the file.' +<a href="{url}">{url}</a><br/><b>{t2}</b><br/> +<a href="{feedback0}">{feedback0}</a><br/> +<a href="mailto:{feedback1}">{feedback1}</a>'''.format( + t0=_('VERSION'), + version=VERSION, + t1=_('REPOSITORY'), + url=Endpoint.repository, + t2=_('FEEDBACK'), + feedback0=Endpoint.feedback_issue, + feedback1=Endpoint.feedback_mail) diff --git a/addons21/fastwq/context.py b/addons21/fastwq/context.py index aed659b..8231c7c 100644 --- a/addons21/fastwq/context.py +++ b/addons21/fastwq/context.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -28,17 +28,15 @@ from .utils import get_icon __all__ = ['APP_ICON', 'config'] - -APP_ICON = get_icon('wqicon.png') # Addon Icon +APP_ICON = get_icon('wqicon.png') # Addon Icon class Config(object): - """ Addon Config """ - _CONFIG_FILENAME = 'fastwqcfg.json' # Config File Path + _CONFIG_FILENAME = 'fastwqcfg.json' # Config File Path def __init__(self, window): self.path = u'_' + self._CONFIG_FILENAME @@ -56,10 +54,12 @@ class Config(object): Update && Save """ data['version'] = VERSION - data['%s_last' % self.pmname] = data.get('last_model', self.last_model_id) + data['%s_last' % self.pmname] = data.get('last_model', + self.last_model_id) self.data.update(data) with open(self.path, 'w', encoding='utf-8') as f: - json.dump(self.data, f, indent=4, sort_keys=True, ensure_ascii=False) + json.dump( + self.data, f, indent=4, sort_keys=True, ensure_ascii=False) f.close() runHook('config.update') @@ -70,13 +70,15 @@ class Config(object): if self.data: return self.data try: - path = self.path if os.path.exists(self.path) else u'.' + self._CONFIG_FILENAME + path = self.path if os.path.exists( + self.path) else u'.' + self._CONFIG_FILENAME with open(path, 'r', encoding="utf-8") as f: self.data = json.load(f) f.close() if not os.path.exists(self.path): self.update(self.data) - except: + except Exception as e: + print('can not find config file', e) self.data = dict() def get_maps(self, model_id): @@ -108,7 +110,7 @@ class Config(object): @property def force_update(self): return self.data.get('force_update', False) - + @property def ignore_mdx_wordcase(self): return self.data.get('ignore_mdx_wordcase', False) @@ -140,4 +142,5 @@ class Config(object): tmpstr = '{{c1::%s}}' return tmpstr + config = Config(mw) diff --git a/addons21/fastwq/gui/__init__.py b/addons21/fastwq/gui/__init__.py index 6febba1..22b5f25 100644 --- a/addons21/fastwq/gui/__init__.py +++ b/addons21/fastwq/gui/__init__.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- from .common import * from .progress import * diff --git a/addons21/fastwq/gui/base.py b/addons21/fastwq/gui/base.py index 6918495..9eb57e3 100644 --- a/addons21/fastwq/gui/base.py +++ b/addons21/fastwq/gui/base.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -18,10 +18,11 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys + from anki.utils import isMac from aqt.qt import * -from ..context import APP_ICON +from ..context import APP_ICON __all__ = ['Dialog', 'WIDGET_SIZE'] @@ -37,20 +38,15 @@ class Dialog(QDialog): return value of the _ui() method, and sets a default title. ''' - self._title = title + self._title = title if "FastWQ" in title else "FastWQ - " + title self._parent = parent super(Dialog, self).__init__(parent) self.setModal(True) self.setWindowFlags( - self.windowFlags() & - ~Qt.WindowContextHelpButtonHint - ) + self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowIcon(APP_ICON) - self.setWindowTitle( - title if "FastWQ" in title - else "FastWQ - " + title - ) + self.setWindowTitle(self._title) # 2 & 3 & mac compatible if isMac and sys.hexversion >= 0x03000000: QApplication.setStyle('Fusion') diff --git a/addons21/fastwq/gui/common.py b/addons21/fastwq/gui/common.py index 3a5d444..69e0083 100644 --- a/addons21/fastwq/gui/common.py +++ b/addons21/fastwq/gui/common.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # diff --git a/addons21/fastwq/gui/dictmanager.py b/addons21/fastwq/gui/dictmanager.py index 768f120..7d38440 100644 --- a/addons21/fastwq/gui/dictmanager.py +++ b/addons21/fastwq/gui/dictmanager.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -19,19 +19,20 @@ import os import sys -from aqt.qt import * + from aqt.forms.editaddon import Ui_Dialog -from .base import Dialog, WIDGET_SIZE -from ..service import service_manager, service_pool +from aqt.qt import * + from ..context import config from ..lang import _, _sl +from ..service import service_manager, service_pool from ..utils import get_icon +from .base import WIDGET_SIZE, Dialog # 2x3 compatible if sys.hexversion >= 0x03000000: unicode = str - __all__ = ['DictManageDialog'] @@ -45,14 +46,14 @@ class DictManageDialog(Dialog): self.main_layout = QVBoxLayout() self.setLayout(self.main_layout) self._options = list() - btnbox = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal , self) + btnbox = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal, self) btnbox.accepted.connect(self.accept) # add dicts mapping self.dicts_layout = QGridLayout() self.main_layout.addLayout(self.dicts_layout) self.main_layout.addWidget(btnbox) self.build() - + def build(self): ''' ''' # labels @@ -80,10 +81,14 @@ class DictManageDialog(Dialog): services = service_manager.local_custom_services + service_manager.web_services for clazz in services: dicts.append({ - 'title': clazz.__title__, - 'unique': clazz.__unique__, - 'path': clazz.__path__, - 'enabled': confs.get(clazz.__unique__, dict()).get('enabled', True) + 'title': + clazz.__title__, + 'unique': + clazz.__unique__, + 'path': + clazz.__path__, + 'enabled': + confs.get(clazz.__unique__, dict()).get('enabled', True) }) # add dict for i, d in enumerate(dicts): @@ -102,7 +107,7 @@ class DictManageDialog(Dialog): ) # button check_btn = QCheckBox(title) - check_btn.setMinimumSize(WIDGET_SIZE.map_dict_width*1.5, 0) + check_btn.setMinimumSize(WIDGET_SIZE.map_dict_width * 1.5, 0) check_btn.setEnabled(True) check_btn.setChecked(enabled) edit_btn = QToolButton(self) @@ -114,9 +119,9 @@ class DictManageDialog(Dialog): self.dicts_layout.addWidget(check_btn, i + 1, 0) self.dicts_layout.addWidget(edit_btn, i + 1, 1) self._options.append({ - 'unique': unique, - 'check_btn': check_btn, - 'edit_btn': edit_btn, + 'unique': unique, + 'check_btn': check_btn, + 'edit_btn': edit_btn, }) def enabled_all_update(self): @@ -126,7 +131,7 @@ class DictManageDialog(Dialog): b = False break self.enabled_all_check_btn.setChecked(b) - + def enabled_all_changed(self): b = self.enabled_all_check_btn.isChecked() for row in self._options: diff --git a/addons21/fastwq/gui/foldermanager.py b/addons21/fastwq/gui/foldermanager.py index 4e08307..9981eb1 100644 --- a/addons21/fastwq/gui/foldermanager.py +++ b/addons21/fastwq/gui/foldermanager.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -18,10 +18,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from aqt.qt import * -from .base import Dialog, WIDGET_SIZE + from ..context import config from ..lang import _, _sl - +from .base import WIDGET_SIZE, Dialog __all__ = ['FoldersManageDialog'] @@ -33,7 +33,7 @@ class FoldersManageDialog(Dialog): def __init__(self, parent, title=u'Dictionary Folder Manager'): super(FoldersManageDialog, self).__init__(parent, title) - #self._dict_paths = [] + # self._dict_paths = [] self.build() def build(self): @@ -65,10 +65,9 @@ class FoldersManageDialog(Dialog): def add_folder(self): dir_ = QFileDialog.getExistingDirectory( self, - caption=u"Select Folder", - directory=config.last_folder, - options=QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks - ) + caption=u"Select Folder", + directory=config.last_folder, + options=QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) if dir_: self.folders_lst.addItem(dir_) config.update({'last_folder': dir_}) @@ -80,8 +79,10 @@ class FoldersManageDialog(Dialog): @property def dirs(self): '''dictionary folders list''' - return [self.folders_lst.item(i).text() - for i in range(self.folders_lst.count())] + return [ + self.folders_lst.item(i).text() + for i in range(self.folders_lst.count()) + ] def accept(self): '''ok button clicked''' diff --git a/addons21/fastwq/gui/options.py b/addons21/fastwq/gui/options.py index d4613af..fefa2e3 100644 --- a/addons21/fastwq/gui/options.py +++ b/addons21/fastwq/gui/options.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -18,22 +18,23 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys + import anki import aqt import aqt.models import sip +from anki.utils import isMac from aqt import mw from aqt.qt import * from aqt.studydeck import StudyDeck -from anki.utils import isMac -from .base import Dialog, WIDGET_SIZE -from .setting import SettingDialog + +from ..constants import Endpoint from ..context import config from ..lang import _, _sl from ..service import service_manager, service_pool -from ..utils import get_model_byId, get_icon -from ..constants import Endpoint - +from ..utils import get_icon, get_model_byId +from .base import WIDGET_SIZE, Dialog +from .setting import SettingDialog __all__ = ['OptionsDialog'] @@ -44,16 +45,13 @@ class OptionsDialog(Dialog): setting query dictionary and fileds ''' - __slot__ = [ - 'begore_build', - 'after_build' - ] + __slot__ = ['begore_build', 'after_build'] _signal = pyqtSignal(str) _NULL_ICON = get_icon('null.png') _OK_ICON = get_icon('ok.png') - def __init__(self, parent, title=u'Options', model_id = -1): + def __init__(self, parent, title=u'Options', model_id=-1): super(OptionsDialog, self).__init__(parent, title) self._signal.connect(self._before_build) self._signal.connect(self._after_build) @@ -61,32 +59,34 @@ class OptionsDialog(Dialog): self.main_layout = QVBoxLayout() self.loading_label = QLabel(_('INITLIZING_DICT')) self.main_layout.addWidget(self.loading_label, 0, Qt.AlignCenter) - #self.loading_layout.addLayout(models_layout) + # self.loading_layout.addLayout(models_layout) self.setLayout(self.main_layout) - #initlize properties + # initlize properties self.model_id = model_id if model_id != -1 else config.last_model_id self.current_model = None self.tabs = [] self.dict_services = None # size and signal - self.resize(WIDGET_SIZE.dialog_width, 4 * WIDGET_SIZE.map_max_height + WIDGET_SIZE.dialog_height_margin) + self.resize( + WIDGET_SIZE.dialog_width, + 4 * WIDGET_SIZE.map_max_height + WIDGET_SIZE.dialog_height_margin) self._signal.emit('before_build') - + def _before_build(self, s): if s != 'before_build': return # dict service list dicts = config.dicts self.dict_services = { - 'local': [], #本地词典 - 'web': [] #网络词典 + 'local': [], # 本地词典 + 'web': [] # 网络词典 } for clazz in service_manager.local_services: if dicts.get(clazz.__unique__, dict()).get('enabled', True): service = service_pool.get(clazz.__unique__) if service and service.support: self.dict_services['local'].append({ - 'title': service.title, + 'title': service.title, 'unique': service.unique }) service_pool.put(service) @@ -95,7 +95,7 @@ class OptionsDialog(Dialog): service = service_pool.get(clazz.__unique__) if service and service.support: self.dict_services['web'].append({ - 'title': service.title, + 'title': service.title, 'unique': service.unique }) service_pool.put(service) @@ -121,13 +121,11 @@ class OptionsDialog(Dialog): # tabs self.tab_widget = QTabWidget() self.tab_widget.setTabBar(CTabBar()) - self.tab_widget.setStyleSheet( - """ + self.tab_widget.setStyleSheet(""" QTabWidget::pane { /* The tab widget frame */ border: 1px solid #c3c3c3; } - """ - ) + """) tab_corner = QWidget() tab_corner_layout = QHBoxLayout() tab_corner_layout.setSpacing(1) @@ -160,8 +158,7 @@ class OptionsDialog(Dialog): # chk_update_btn = QPushButton(_('UPDATE')) # chk_update_btn.clicked.connect(self.check_updates) home_label = QLabel( - '<a href="{url}">User Guide</a>'.format(url=Endpoint.user_guide) - ) + '<a href="{url}">User Guide</a>'.format(url=Endpoint.user_guide)) home_label.setOpenExternalLinks(True) # buttons btnbox = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal, self) @@ -171,7 +168,7 @@ class OptionsDialog(Dialog): bottom_layout.addWidget(about_btn) bottom_layout.addWidget(home_label) bottom_layout.addWidget(btnbox) - #self.setLayout(self.main_layout) + # self.setLayout(self.main_layout) self.main_layout.addLayout(bottom_layout) # init from saved data self.current_model = None @@ -179,7 +176,8 @@ class OptionsDialog(Dialog): self.current_model = get_model_byId(mw.col.models, self.model_id) if self.current_model: self.models_button.setText( - u'%s [%s]' % (_('CHOOSE_NOTE_TYPES'), self.current_model['name'])) + u'%s [%s]' % (_('CHOOSE_NOTE_TYPES'), + self.current_model['name'])) # build fields -- dicts layout self.build_tabs_layout() @@ -198,7 +196,7 @@ class OptionsDialog(Dialog): '''open folder manager dialog''' self.accept() self.setResult(1001) - + def show_dm_dialog(self): '''open dictionary manager dialog''' self.accept() @@ -225,13 +223,18 @@ class OptionsDialog(Dialog): ''' build dictionary、fields etc ''' - try: self.tab_widget.currentChanged.disconnect() - except Exception: pass + try: + self.tab_widget.currentChanged.disconnect() + except Exception: + pass while len(self.tabs) > 0: self.removeTab(0, True) # tabs conf = config.get_maps(self.current_model['id']) - maps_list = {'list': [conf], 'def': 0} if isinstance(conf, list) else conf + maps_list = { + 'list': [conf], + 'def': 0 + } if isinstance(conf, list) else conf for maps in maps_list['list']: self.addTab(maps, False) self.tab_widget.currentChanged.connect(self.changedTab) @@ -240,28 +243,21 @@ class OptionsDialog(Dialog): self.tab_widget.setCurrentIndex(maps_list['def']) # size self.resize( - WIDGET_SIZE.dialog_width, - min(max(3, len(self.current_model['flds'])+1), 14) * WIDGET_SIZE.map_max_height + WIDGET_SIZE.dialog_height_margin - ) + WIDGET_SIZE.dialog_width, + min(max(3, + len(self.current_model['flds']) + 1), 14) * + WIDGET_SIZE.map_max_height + WIDGET_SIZE.dialog_height_margin) self.save() - + def addTab(self, maps=None, forcus=True): i = len(self.tabs) if isinstance(maps, list): - maps = { - 'fields': maps, - 'name': _('CONFIG_INDEX') % (i+1) - } - tab = TabContent( - self.current_model, - maps['fields'] if maps else None, - self.dict_services - ) + maps = {'fields': maps, 'name': _('CONFIG_INDEX') % (i + 1)} + tab = TabContent(self.current_model, maps['fields'] if maps else None, + self.dict_services) self.tabs.append(tab) self.tab_widget.addTab( - tab, - maps['name'] if maps else _('CONFIG_INDEX') % (i+1) - ) + tab, maps['name'] if maps else _('CONFIG_INDEX') % (i + 1)) if forcus: self.tab_widget.setCurrentIndex(i) @@ -273,7 +269,7 @@ class OptionsDialog(Dialog): del self.tabs[i] self.tab_widget.removeTab(i) tab.destroy() - #for k in range(0, len(self.tabs)): + # for k in range(0, len(self.tabs)): # self.tab_widget.setTabText(k, _('CONFIG_INDEX') % (k+1)) def changedTab(self, i): @@ -289,12 +285,18 @@ class OptionsDialog(Dialog): ''' show choose note type window ''' - edit = QPushButton(anki.lang._("Manage"), - clicked=lambda: aqt.models.Models(mw, self)) - ret = StudyDeck(mw, names=lambda: sorted(mw.col.models.allNames()), - accept=anki.lang._("Choose"), title=anki.lang._("Choose Note Type"), - help="_notes", parent=self, buttons=[edit], - cancel=True, geomKey="selectModel") + edit = QPushButton( + anki.lang._("Manage"), clicked=lambda: aqt.models.Models(mw, self)) + ret = StudyDeck( + mw, + names=lambda: sorted(mw.col.models.allNames()), + accept=anki.lang._("Choose"), + title=anki.lang._("Choose Note Type"), + help="_notes", + parent=self, + buttons=[edit], + cancel=True, + geomKey="selectModel") if ret.name: model = mw.col.models.byName(ret.name) self.models_button.setText( @@ -306,18 +308,17 @@ class OptionsDialog(Dialog): if not self.current_model: return data = dict() - maps_list = { - 'list': [], - 'def': self.tab_widget.currentIndex() - } + maps_list = {'list': [], 'def': self.tab_widget.currentIndex()} for i, tab in enumerate(self.tabs): maps_list['list'].append({ - 'fields': tab.data, - 'name': self.tab_widget.tabBar().tabText(i) + 'fields': + tab.data, + 'name': + self.tab_widget.tabBar().tabText(i) }) current_model_id = str(self.current_model['id']) data[current_model_id] = maps_list - data['last_model'] = self.current_model['id'] + data['last_model'] = self.current_model['id'] config.update(data) @@ -339,7 +340,7 @@ class TabContent(QScrollArea): self.setWidgetResizable(True) self.setWidget(dicts) self.dicts_layout = dicts.layout() - #self.dicts_layout.setSizeConstraint(QLayout.SetFixedSize) + # self.dicts_layout.setSizeConstraint(QLayout.SetFixedSize) def build_layout(self): ''' @@ -352,7 +353,7 @@ class TabContent(QScrollArea): self._last_checkeds = None self._was_built = True - model = self._model + model = self._model maps = self._conf # labels @@ -372,7 +373,8 @@ class TabContent(QScrollArea): self.ignore_all_check_btn.setEnabled(True) self.ignore_all_check_btn.setChecked(True) self.dicts_layout.addWidget(self.ignore_all_check_btn, 0, 1) - self.ignore_all_check_btn.clicked.connect(self.ignore_all_check_changed) + self.ignore_all_check_btn.clicked.connect( + self.ignore_all_check_changed) # Skip valued all self.skip_all_check_btn = QCheckBox(_('SELECT_ALL')) @@ -381,7 +383,7 @@ class TabContent(QScrollArea): self.skip_all_check_btn.setChecked(True) self.dicts_layout.addWidget(self.skip_all_check_btn, 0, 4) self.skip_all_check_btn.clicked.connect(self.skip_all_check_changed) - + # dict & fields self.radio_group = QButtonGroup() for i, fld in enumerate(model['flds']): @@ -389,16 +391,19 @@ class TabContent(QScrollArea): name = fld['name'] if maps: for j, each in enumerate(maps): - if each.get('fld_ord', -1) == ord or each.get('fld_name', '') == name: + if each.get('fld_ord', -1) == ord or each.get( + 'fld_name', '') == name: each['fld_name'] = name each['fld_ord'] = ord self.add_dict_layout(j, **each) break else: - self.add_dict_layout(i, fld_name=name, fld_ord=ord, word_checked=i==0) + self.add_dict_layout( + i, fld_name=name, fld_ord=ord, word_checked=i == 0) else: - self.add_dict_layout(i, fld_name=name, fld_ord=ord, word_checked=i==0) - + self.add_dict_layout( + i, fld_name=name, fld_ord=ord, word_checked=i == 0) + # update self.ignore_all_update() self.skip_all_update() @@ -410,21 +415,21 @@ class TabContent(QScrollArea): word_checked = kwargs.get('word_checked', False) fld_name, fld_ord = ( - kwargs.get('fld_name', ''), #笔记类型的字段名 - kwargs.get('fld_ord', ''), #笔记类型的字段编号 + kwargs.get('fld_name', ''), # 笔记类型的字段名 + kwargs.get('fld_ord', ''), # 笔记类型的字段编号 ) dict_name, dict_unique, dict_fld_name, dict_fld_ord = ( - kwargs.get('dict_name', ''), #字典名 - kwargs.get('dict_unique', ''), #字典ID - kwargs.get('dict_fld_name', ''), #对应字典的字段名 - kwargs.get('dcit_fld_ord', 0) #对应字典的字段编号 + kwargs.get('dict_name', ''), # 字典名 + kwargs.get('dict_unique', ''), # 字典ID + kwargs.get('dict_fld_name', ''), # 对应字典的字段名 + kwargs.get('dcit_fld_ord', 0) # 对应字典的字段编号 ) ignore, skip, cloze = ( - kwargs.get('ignore', True), #忽略标志 - kwargs.get('skip_valued', True), #略过有值项标志 - kwargs.get('cloze_word', False), #单词填空 + kwargs.get('ignore', True), # 忽略标志 + kwargs.get('skip_valued', True), # 略过有值项标志 + kwargs.get('cloze_word', False), # 单词填空 ) # check @@ -436,26 +441,23 @@ class TabContent(QScrollArea): # dict combox dict_combo = QComboBox() dict_combo.setMinimumSize(WIDGET_SIZE.map_dict_width, 0) - dict_combo.setMaximumSize( - WIDGET_SIZE.map_dict_width, - WIDGET_SIZE.map_max_height - ) - dict_combo.setFocusPolicy( - Qt.TabFocus | Qt.ClickFocus | Qt.StrongFocus | Qt.WheelFocus - ) - ignore = not self.fill_dict_combo_options(dict_combo, dict_unique, self._services) or ignore + dict_combo.setMaximumSize(WIDGET_SIZE.map_dict_width, + WIDGET_SIZE.map_max_height) + dict_combo.setFocusPolicy(Qt.TabFocus | Qt.ClickFocus | Qt.StrongFocus + | Qt.WheelFocus) + ignore = not self.fill_dict_combo_options(dict_combo, dict_unique, + self._services) or ignore dict_unique = dict_combo.itemData(dict_combo.currentIndex()) dict_combo.setEnabled(not word_checked and not ignore) # field combox field_combo = QComboBox() field_combo.setMinimumSize(WIDGET_SIZE.map_field_width, 0) - field_combo.setMaximumSize( - WIDGET_SIZE.map_field_width, - WIDGET_SIZE.map_max_height - ) + field_combo.setMaximumSize(WIDGET_SIZE.map_field_width, + WIDGET_SIZE.map_max_height) field_combo.setEnabled(not word_checked and not ignore) - self.fill_field_combo_options(field_combo, dict_name, dict_unique, dict_fld_name, dict_fld_ord) - + self.fill_field_combo_options(field_combo, dict_name, dict_unique, + dict_fld_name, dict_fld_ord) + # ignore ignore_check_btn = QCheckBox(_("NOT_DICT_FIELD")) ignore_check_btn.setEnabled(not word_checked) @@ -489,13 +491,14 @@ class TabContent(QScrollArea): cloze_check_btn.setEnabled(not word_checked and not ignore) if word_checked: self._last_checkeds = [ - ignore_check_btn, dict_combo, - field_combo, skip_check_btn + ignore_check_btn, dict_combo, field_combo, skip_check_btn ] + word_check_btn.clicked.connect(radio_btn_checked) - if word_checked: + if word_checked: self._last_checkeds = None radio_btn_checked() + # ignor def ignore_check_changed(): word_checked = word_check_btn.isChecked() @@ -504,35 +507,38 @@ class TabContent(QScrollArea): field_combo.setEnabled(not word_checked and not ignore) skip_check_btn.setEnabled(not word_checked and not ignore) cloze_check_btn.setEnabled(not word_checked and not ignore) + ignore_check_btn.stateChanged.connect(ignore_check_changed) ignore_check_btn.clicked.connect(self.ignore_all_update) # skip skip_check_btn.clicked.connect(self.skip_all_update) + # dict def dict_combo_changed(index): '''dict combo box index changed''' self.fill_field_combo_options( - field_combo, - dict_combo.currentText(), - dict_combo.itemData(index), - field_combo.currentText(), - field_combo.itemData(field_combo.currentIndex()) - ) + field_combo, dict_combo.currentText(), + dict_combo.itemData(index), field_combo.currentText(), + field_combo.itemData(field_combo.currentIndex())) + dict_combo.currentIndexChanged.connect(dict_combo_changed) - + self.dicts_layout.addWidget(word_check_btn, i + 1, 0) self.dicts_layout.addWidget(ignore_check_btn, i + 1, 1) self.dicts_layout.addWidget(dict_combo, i + 1, 2) self.dicts_layout.addWidget(field_combo, i + 1, 3) self.dicts_layout.addWidget(skip_check_btn, i + 1, 4) self.dicts_layout.addWidget(cloze_check_btn, i + 1, 5) - + self._options.append({ - 'model': {'fld_name': fld_name, 'fld_ord': fld_ord}, - 'word_check_btn': word_check_btn, - 'dict_combo': dict_combo, - 'field_combo': field_combo, - 'ignore_check_btn': ignore_check_btn, + 'model': { + 'fld_name': fld_name, + 'fld_ord': fld_ord + }, + 'word_check_btn': word_check_btn, + 'dict_combo': dict_combo, + 'field_combo': field_combo, + 'ignore_check_btn': ignore_check_btn, 'skip_check_btn': skip_check_btn, 'cloze_check_btn': cloze_check_btn }) @@ -548,7 +554,7 @@ class TabContent(QScrollArea): # hr if len(services['local']) > 0: dict_combo.insertSeparator(dict_combo.count()) - + # web dict service for service in services['web']: dict_combo.addItem(service['title'], userData=service['unique']) @@ -561,15 +567,18 @@ class TabContent(QScrollArea): dict_combo.setCurrentIndex(i) return True return False + return set_dict_combo_index() - def fill_field_combo_options(self, field_combo, dict_combo_text, dict_combo_itemdata, dict_fld_name, dict_fld_ord): + def fill_field_combo_options(self, field_combo, dict_combo_text, + dict_combo_itemdata, dict_fld_name, + dict_fld_ord): '''setup field combobox''' field_combo.clear() field_combo.setEditable(False) - #if dict_combo_text in _sl('NOT_DICT_FIELD'): + # if dict_combo_text in _sl('NOT_DICT_FIELD'): # field_combo.setEnabled(False) - #el + # el if dict_combo_text in _sl('MDX_SERVER'): text = dict_fld_name if dict_fld_name else 'http://' field_combo.setEditable(True) @@ -594,16 +603,26 @@ class TabContent(QScrollArea): maps = [] for row in self._options: maps.append({ - 'fld_name': row['model']['fld_name'], - 'fld_ord': row['model']['fld_ord'], - 'word_checked': row['word_check_btn'].isChecked(), - 'dict_name': row['dict_combo'].currentText().strip(), - 'dict_unique': row['dict_combo'].itemData(row['dict_combo'].currentIndex()), - 'dict_fld_name': row['field_combo'].currentText().strip(), - 'dict_fld_ord': row['field_combo'].itemData(row['field_combo'].currentIndex()), - 'ignore': row['ignore_check_btn'].isChecked(), - 'skip_valued': row['skip_check_btn'].isChecked(), - 'cloze_word': row['cloze_check_btn'].isChecked() + 'fld_name': + row['model']['fld_name'], + 'fld_ord': + row['model']['fld_ord'], + 'word_checked': + row['word_check_btn'].isChecked(), + 'dict_name': + row['dict_combo'].currentText().strip(), + 'dict_unique': + row['dict_combo'].itemData(row['dict_combo'].currentIndex()), + 'dict_fld_name': + row['field_combo'].currentText().strip(), + 'dict_fld_ord': + row['field_combo'].itemData(row['field_combo'].currentIndex()), + 'ignore': + row['ignore_check_btn'].isChecked(), + 'skip_valued': + row['skip_check_btn'].isChecked(), + 'cloze_word': + row['cloze_check_btn'].isChecked() }) return maps @@ -624,7 +643,7 @@ class TabContent(QScrollArea): b = False break self.ignore_all_check_btn.setChecked(b) - + def skip_all_update(self): b = True for row in self._options: @@ -635,8 +654,7 @@ class TabContent(QScrollArea): class CTabBar(QTabBar): - - def __init__(self, parent = None): + def __init__(self, parent=None): super(CTabBar, self).__init__(parent) # style self.setTabsClosable(True) @@ -651,14 +669,17 @@ class CTabBar(QTabBar): self._editor.installEventFilter(self) def eventFilter(self, widget, event): - if ((event.type() == QEvent.MouseButtonPress and \ - not self._editor.geometry().contains(event.globalPos()) \ - ) or \ - (event.type() == QEvent.KeyPress and \ - event.key() == Qt.Key_Escape) - ): - self.hideEditor() - return True + bhide = False + if event.type() == QEvent.MouseButtonPress: + if not self._editor.geometry().contains(event.globalPos()): + bhide = True + if not bhide: + if event.type() == QEvent.KeyPress: + if event.key() == Qt.Key_Escape: + bhide = True + if bhide: + self.hideEditor() + return True return QTabBar.eventFilter(self, widget, event) def mouseDoubleClickEvent(self, event): @@ -676,7 +697,7 @@ class CTabBar(QTabBar): self._editor.selectAll() self._editor.setEnabled(True) self._editor.setFocus() - + def hideEditor(self): if self._editor.isVisible(): self._editor.setEnabled(False) diff --git a/addons21/fastwq/gui/progress.py b/addons21/fastwq/gui/progress.py index cedb98e..422dd44 100644 --- a/addons21/fastwq/gui/progress.py +++ b/addons21/fastwq/gui/progress.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -17,24 +17,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - import time from collections import defaultdict from aqt.qt import * -from ..lang import _ -from ..context import APP_ICON +from ..context import APP_ICON +from ..lang import _ __all__ = ['ProgressWindow'] - _INFO_TEMPLATE = u''.join([ u'<strong>' + _('QUERIED') + u'</strong>', u'<p>' + 45 * u'-' + u'</p>', u'<p>' + _('SUCCESS') + u' <b>{}</b> ' + _('WORDS') + u'</p>', u'<p>' + _('SKIPED') + u' <b>{}</b> ' + _('WORDS') + u'</p>', - u'<p>' + _('UPDATE') + u' <b>{}</b> ' + _('FIELDS') + u'</p>', + u'<p>' + _('UPDATE') + u' <b>{}</b> ' + _('FIELDS') + u'</p>', u'<p>' + _('FAILURE') + u' <b>{}</b> ' + _('WORDS') + u'</p>', ]) @@ -66,15 +64,12 @@ class ProgressWindow(object): self._msg_count.get('words_number', 0), self._msg_count.get('fields_number', 0), self._msg_count.get('fails_number', 0), - self._msg_count.get('skips_number', 0) - ) - number_info = _INFO_TEMPLATE.format( - words_number, - skips_number, - fields_number, - fails_number - ) - self._update(label=number_info, value=words_number+skips_number+fails_number) + self._msg_count.get('skips_number', 0)) + number_info = _INFO_TEMPLATE.format(words_number, skips_number, + fields_number, fails_number) + self._update( + label=number_info, + value=words_number + skips_number + fails_number) self._win.adjustSize() self.app.processEvents() @@ -95,9 +90,7 @@ class ProgressWindow(object): self._win.setWindowTitle("FastWQ - Querying...") self._win.setModal(True) self._win.setWindowFlags( - self._win.windowFlags() & - ~Qt.WindowContextHelpButtonHint - ) + self._win.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._win.setWindowIcon(APP_ICON) self._win.setAutoReset(True) self._win.setAutoClose(True) diff --git a/addons21/fastwq/gui/setting.py b/addons21/fastwq/gui/setting.py index 842454d..8327560 100644 --- a/addons21/fastwq/gui/setting.py +++ b/addons21/fastwq/gui/setting.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -21,7 +21,7 @@ from aqt.qt import * from ..context import config from ..lang import _ -from .base import WIDGET_SIZE, Dialog +from .base import Dialog __all__ = ['SettingDialog'] @@ -89,23 +89,23 @@ class SettingDialog(Dialog): hbox.addSpacing(300) hbox.addWidget(resetbtn) hbox.addWidget(okbtn) - + layout.addSpacing(48) layout.addLayout(hbox) - + self.check_force_update = check_force_update self.check_ignore_accents = check_ignore_accents self.check_ighore_mdx_wordcase = check_ighore_mdx_wordcase self.input_thread_number = input_thread_number self.input_cloze_str = input_cloze_str - layout.setAlignment(Qt.AlignTop|Qt.AlignLeft) + layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.setLayout(layout) def accept(self): self.save() super(SettingDialog, self).accept() - + def reset(self): data = { 'force_update': False, @@ -120,7 +120,7 @@ class SettingDialog(Dialog): self.check_ighore_mdx_wordcase.setChecked(config.ignore_mdx_wordcase) self.input_thread_number.setValue(config.thread_number) self.input_cloze_str.setText(config.cloze_str) - + def save(self): data = { 'force_update': self.check_force_update.isChecked(), diff --git a/addons21/fastwq/lang.py b/addons21/fastwq/lang.py index 6b791b4..f8e2df5 100644 --- a/addons21/fastwq/lang.py +++ b/addons21/fastwq/lang.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -18,15 +18,15 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from anki.lang import currentLang + try: - basestring + basestring except NameError: - basestring = str + basestring = str __all__ = ['_', '_cl', '_sl'] - -#Language Define, [Key, zh_CN, en] +# Language Define, [Key, zh_CN, en] _arr = [ ['CHECK_FILENAME_LABEL', u'使用文件名作为标签', u'Use the Filename as Label'], ['EXPORT_MEDIA', u'导出媒体文件', u'Export Media Files'], @@ -42,13 +42,19 @@ _arr = [ ['QUERIED', u'查询', u'Queried'], ['FIELDS', u'字段', u'Fields'], ['WORDS', u'单词', u'Words'], - ['NOT_DICT_FIELD', u'忽略', u'Ignore'], #不是字典字段 + ['NOT_DICT_FIELD', u'忽略', u'Ignore'], # 不是字典字段 ['NOTE_TYPE_FIELDS', u'<b>笔记字段</b>', u'<b>Note Fields</b>'], ['DICTS', u'<b>字典</b>', u'<b>Dictionary</b>'], ['DICT_FIELDS', u'<b>字典字段</b>', u'<b>Fields</b>'], - ['RADIOS_DESC', u'<b>单选框选中为待查询单词字段</b>', u'<b> Select the field to be queried with single selection.</b>'], + [ + 'RADIOS_DESC', u'<b>单选框选中为待查询单词字段</b>', + u'<b> Select the field to be queried with single selection.</b>' + ], ['NO_QUERY_WORD', u'查询字段无单词', u'The query field is empty'], - ['CSS_NOT_FOUND', u'没有找到CSS文件,请手动选择', u'No CSS file found, please select one manually.'], + [ + 'CSS_NOT_FOUND', u'没有找到CSS文件,请手动选择', + u'No CSS file found, please select one manually.' + ], ['ABOUT', u'关于', u'About'], ['REPOSITORY', u'项目地址', u'Project Repo'], ['FEEDBACK', u'反馈', u'Feedback'], @@ -60,7 +66,10 @@ _arr = [ ['UPDATE', u'更新', u'Update'], ['AUTO_UPDATE', u'自动检测新版本', u'Auto check new version'], ['CHECK_UPDATE', u'检测更新', u'Check Update'], - ['IGNORE_MDX_WORDCASE', u'忽略本地词典单词大小写', u'Ignore MDX dictionary word case'], + [ + 'IGNORE_MDX_WORDCASE', u'忽略本地词典单词大小写', + u'Ignore MDX dictionary word case' + ], ['FORCE_UPDATE', u'强制更新字段', u'Forced Updates of all fields'], ['IGNORE_ACCENTS', u'忽略声调', u'Ignore Accents'], ['SKIP_VALUED', u'跳过有值项', u'Skip non-empty'], @@ -68,7 +77,10 @@ _arr = [ ['SETTINGS', u'参数', u'Settings'], ['THREAD_NUMBER', u'线程数', u'Number of Threads'], ['INITLIZING_DICT', u'初始化词典...', u'Initlizing Dictionary...'], - ['PLS_SET_DICTIONARY_FIELDS', u'请设置字典和字段', u'Please set the dictionary and fields.'], + [ + 'PLS_SET_DICTIONARY_FIELDS', u'请设置字典和字段', + u'Please set the dictionary and fields.' + ], ['CONFIG_INDEX', u'配置 %s', u'Config %s'], ['SELECT_ALL', u'全选', u'All'], ['DICTS_NAME', u'字典名称', u'Dictionary Name'], @@ -80,7 +92,6 @@ _arr = [ ['OPTIONS', u'选项', u'Options'], ['CLOZE_WORD', u'单词填空', u'Cloze word'], ['CLOZE_WORD_FORMAT', '单词填空格式', 'Cloze word formater'], - ['BRE_PRON', u'英式发音', u'British Pronunciation'], ['AME_PRON', u'美式发音', u'American Pronunciation'], ['PRON', u'发音', u'Audio Pronunciation'], @@ -103,7 +114,7 @@ def _(key, lang=currentLang): def disp(s): return s.lower().capitalize() - + if key not in _trans or lang not in _trans[key]: return disp(key) return _trans[key][lang] From b07d343dc3ce3e50167c0babe03283fe9975fcd6 Mon Sep 17 00:00:00 2001 From: "St.Huang" <wenhonghuang@gmail.com> Date: Sun, 10 Feb 2019 11:44:42 +0800 Subject: [PATCH 4/6] fix #92 --- addons21/fastwq/query/common.py | 94 ++++++++++++++++----------------- addons21/fastwq/service/base.py | 87 ++++++++++++++++-------------- 2 files changed, 91 insertions(+), 90 deletions(-) diff --git a/addons21/fastwq/query/common.py b/addons21/fastwq/query/common.py index 0881898..fa0a647 100644 --- a/addons21/fastwq/query/common.py +++ b/addons21/fastwq/query/common.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -17,29 +17,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - +import io import os import re -import io import shutil import unicodedata - from collections import defaultdict + from aqt.qt import * from aqt.utils import showInfo from ..constants import Template from ..context import config -from ..service import service_pool, QueryResult, copy_static_file +from ..libs.snowballstemmer import stemmer +from ..service import QueryResult, copy_static_file, service_pool from ..service.base import LocalService from ..utils import wrap_css -from ..libs.snowballstemmer import stemmer - __all__ = [ - 'InvalidWordException', 'update_note_fields', - 'update_note_field', 'promot_choose_css', 'add_to_tmpl', - 'query_flds', 'inspect_note' + 'InvalidWordException', 'update_note_fields', 'update_note_field', + 'promot_choose_css', 'add_to_tmpl', 'query_flds', 'inspect_note' ] @@ -127,19 +124,14 @@ def promot_choose_css(missed_css): if not os.path.exists(filename) and not css['file'] in checked: checked.add(css['file']) showInfo( - Template.miss_css.format( - dict=css['title'], - css=css['file'] - ) - ) + Template.miss_css.format(dict=css['title'], css=css['file'])) try: - filepath = css['dict_path'][:css['dict_path'].rindex( - os.path.sep)+1] + filepath = css['dict_path'][:css['dict_path'].rindex(os.path. + sep) + 1] filepath = QFileDialog.getOpenFileName( directory=filepath, caption=u'Choose css file', - filter=u'CSS (*.css)' - ) + filter=u'CSS (*.css)') if filepath: shutil.copy(filepath, filename) wrap_css(filename) @@ -161,15 +153,17 @@ def add_to_tmpl(note, **kwargs): if js and js.strip(): addings = js.strip() if addings not in afmt: - if not addings.startswith(u'<script') and not addings.endswith(u'/script>'): - addings = u'\n<script type="text/javascript">\n{}\n</script>'.format(addings) + if not addings.startswith(u'<script') and not addings.endswith( + u'/script>'): + addings = u'\n<script type="text/javascript">\n{}\n</script>'.format( + addings) afmt += addings if jsfile: - #new_jsfile = u'_' + \ - # jsfile if not jsfile.startswith(u'_') else jsfile - #copy_static_file(jsfile, new_jsfile) - #addings = u'\r\n<script src="{}"></script>'.format(new_jsfile) - #afmt += addings + # new_jsfile = u'_' + \ + # jsfile if not jsfile.startswith(u'_') else jsfile + # copy_static_file(jsfile, new_jsfile) + # addings = u'\r\n<script src="{}"></script>'.format(new_jsfile) + # afmt += addings jsfile = jsfile if isinstance(jsfile, list) else [jsfile] for fn in jsfile: addings = ''' @@ -204,17 +198,17 @@ def query_flds(note, fileds=None): continue if i == len(note.fields): break - #ignore field + # ignore field ignore = each.get('ignore', False) if ignore: continue - #skip valued + # skip valued skip = each.get('skip_valued', False) if skip and len(note.fields[i]) != 0: continue - #cloze + # cloze cloze = each.get('cloze_word', False) - #normal + # normal dict_unique = each.get('dict_unique', '').strip() dict_fld_ord = each.get('dict_fld_ord', -1) fld_ord = each.get('fld_ord', -1) @@ -227,9 +221,9 @@ def query_flds(note, fileds=None): services[dict_unique] = s if s and s.support: tasks.append({ - 'k': dict_unique, + 'k': dict_unique, 'w': word, - 'f': dict_fld_ord, + 'f': dict_fld_ord, 'i': fld_ord, 'cloze': cloze, }) @@ -237,17 +231,17 @@ def query_flds(note, fileds=None): success_num = 0 result = defaultdict(QueryResult) for task in tasks: - #try: - service = services.get(task['k'], None) - qr = service.active(task['f'], task['w']) - if qr: - if task['cloze']: - qr['result'] = cloze_deletion(qr['result'], word) - result.update({task['i']: qr}) - success_num += 1 - #except: - # showInfo(_("NO_QUERY_WORD")) - # pass + try: + service = services.get(task['k'], None) + qr = service.active(task['f'], task['w']) + if qr: + if task['cloze']: + qr['result'] = cloze_deletion(qr['result'], word) + result.update({task['i']: qr}) + success_num += 1 + except Exception as e: + print(_("NO_QUERY_WORD"), e) + pass missed_css = list() for service in services.values(): @@ -284,13 +278,15 @@ def cloze_deletion(text, cloze): continue word = text[s:e] if _stemmer.stemWord(word).lower() == term: - l = len(cloze) + ln = len(cloze) w = word - if w[:l].lower() == cloze.lower(): - e = s + l - w = word[:l] - result = result[:s+offset] + (config.cloze_str % w) + result[e+offset:] - offset += len(config.cloze_str)-2 + if w[:ln].lower() == cloze.lower(): + e = s + ln + w = word[:ln] + result = result[:s + offset] + ( + config.cloze_str % w) + result[e + offset:] + offset += len(config.cloze_str) - 2 return result + _stemmer = stemmer('english') diff --git a/addons21/fastwq/service/base.py b/addons21/fastwq/service/base.py index e593308..d78f26b 100644 --- a/addons21/fastwq/service/base.py +++ b/addons21/fastwq/service/base.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # # Copyright (C) 2018 sthoo <sth201807@gmail.com> # @@ -19,6 +19,7 @@ import inspect import os +import random # use ntpath module to ensure the windows-style (e.g. '\\LDOCE.css') # path can be processed on Unix platform. # However, anki version on mac platforms doesn't including this package? @@ -27,32 +28,33 @@ import re import shutil import sqlite3 import urllib +import zlib +from collections import defaultdict +from functools import wraps +from hashlib import md5, sha1 + import requests +from bs4 import BeautifulSoup + +from aqt import mw +from aqt.qt import QMutex, QThread + +from ..context import config +from ..lang import _cl +from ..libs import MdxBuilder, StardictBuilder +from ..utils import MapDict, wrap_css try: import urllib2 -except: +except Exception: import urllib.request as urllib2 -import zlib -import random -from collections import defaultdict -from functools import wraps -from hashlib import md5 -from hashlib import sha1 try: from cookielib import CookieJar -except: +except Exception: from http.cookiejar import CookieJar -from aqt import mw -from aqt.qt import QThread, QMutex -from bs4 import BeautifulSoup -from ..context import config -from ..libs import MdxBuilder, StardictBuilder -from ..utils import MapDict, wrap_css -from ..lang import _cl try: import threading as _threading @@ -69,18 +71,14 @@ __all__ = [ def get_hex_name(prefix, val, suffix): ''' get sha1 hax name ''' hex_digest = sha1(val.encode('utf-8')).hexdigest().lower() - name = '.'.join([ - '-'.join([ - prefix, hex_digest[:8], hex_digest[8:16], - hex_digest[16:24], hex_digest[24:32], hex_digest[32:], - ]), - suffix, - ]) + name = '.'.join(['-'.join([prefix, hex_digest[:8], hex_digest[8:16], hex_digest[16:24], hex_digest[24:32], hex_digest[32:], ]), suffix, ]) return name + def _is_method_or_func(object): return inspect.isfunction(object) or inspect.ismethod(object) + def register(labels): """ register the dict service with a labels, which will be shown in the dicts list. @@ -120,6 +118,8 @@ def export(labels): export.EXPORT_INDEX += 1 return _deco return _with + + export.EXPORT_INDEX = 0 @@ -181,8 +181,9 @@ def with_styles(**styles): return _deco return _with -# bs4 threading lock, overload protection -_BS_LOCKS = [_threading.Lock(), _threading.Lock()] + +_BS_LOCKS = [_threading.Lock(), _threading.Lock()] # bs4 threading lock, overload protection + def parse_html(html): ''' @@ -238,6 +239,7 @@ class Service(object): @property def unique(self): return self._unique + @unique.setter def unique(self, value): self._unique = value @@ -326,7 +328,7 @@ class WebService(Service): if response.info().get('Content-Encoding') == 'gzip': data = zlib.decompress(data, 16 + zlib.MAX_WBITS) return data - except: + except Exception: return u'' @classmethod @@ -340,7 +342,7 @@ class WebService(Service): '(KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36' }).content) return True - except Exception as e: + except Exception: pass class TinyDownloadError(ValueError): @@ -434,7 +436,7 @@ class WebService(Service): try: value_error.payload = response.read() response.close() - except: + except Exception: pass raise value_error @@ -475,7 +477,7 @@ class WebService(Service): f.write(payload) f.close() return True - except: + except Exception: return False @@ -516,7 +518,7 @@ class LocalService(Service): def _get_builer(key, func=None): LocalService._mutex_builder.lock() key = md5(str(key).encode('utf-8')).hexdigest() - if not func is None: + if not(func is None): if not LocalService._mdx_builders[key]: worker = _DictBuildWorker(func) worker.start() @@ -582,21 +584,22 @@ class MdxService(LocalService): jsfile = re.findall(r'<script .*?src=[\'\"](.+?)[\'\"]', html, re.DOTALL) return QueryResult(result=html, js=u'\n'.join(js), jsfile=jsfile) - def _get_definition_mdx(self): """according to the word return mdx dictionary page""" - content = self.builder.mdx_lookup(self.word, ignorecase=config.ignore_mdx_wordcase) + ignorecase = config.ignore_mdx_wordcase and (self.word != self.word.lower() or self.word != self.word.upper()) + content = self.builder.mdx_lookup(self.word, ignorecase=ignorecase) str_content = "" if len(content) > 0: for c in content: - str_content += c.replace("\r\n","").replace("entry:/","") + str_content += c.replace("\r\n", "").replace("entry:/", "") return str_content def _get_definition_mdd(self, word): """according to the keyword(param word) return the media file contents""" word = word.replace('/', '\\') - content = self.builder.mdd_lookup(word, ignorecase=config.ignore_mdx_wordcase) + ignorecase = config.ignore_mdx_wordcase and (word != word.lower() or word != word.upper()) + content = self.builder.mdd_lookup(word, ignorecase=ignorecase) if len(content) > 0: return [content[0]] else: @@ -620,7 +623,7 @@ class MdxService(LocalService): f.write(bytes_list[0]) return savepath except sqlite3.OperationalError as e: - #showInfo(str(e)) + print(e) pass return '' @@ -677,7 +680,7 @@ class MdxService(LocalService): # folder first, and it will also execute the wrap process to generate # the desired file. if not os.path.exists(cssfile): - css_src = self.dict_path.replace(self._filename+u'.mdx', f) + css_src = self.dict_path.replace(self._filename + u'.mdx', f) if os.path.exists(css_src): shutil.copy(css_src, cssfile) else: @@ -700,17 +703,19 @@ class MdxService(LocalService): if os.path.exists(savepath): return savepath try: - src_fn = self.dict_path.replace(self._filename+u'.mdx', basename) + src_fn = self.dict_path.replace(self._filename + u'.mdx', basename) if os.path.exists(src_fn): shutil.copy(src_fn, savepath) return savepath else: - bytes_list = self.builder.mdd_lookup(filepath_in_mdx, ignorecase=config.ignore_mdx_wordcase) + ignorecase = config.ignore_mdx_wordcase and (filepath_in_mdx != filepath_in_mdx.lower() or filepath_in_mdx != filepath_in_mdx.upper()) + bytes_list = self.builder.mdd_lookup(filepath_in_mdx, ignorecase=ignorecase) if bytes_list: with open(savepath, 'wb') as f: f.write(bytes_list[0]) return savepath except sqlite3.OperationalError as e: + print('save default file error', e) pass def save_media_files(self, data): @@ -752,7 +757,7 @@ class StardictService(LocalService): dict_path, service_wrap(StardictBuilder, dict_path, in_memory=False) ) - #if self.builder: + # if self.builder: # self.builder.get_header() @staticmethod @@ -772,7 +777,7 @@ class StardictService(LocalService): @export([u'默认', u'Default']) def fld_whole(self): - #self.builder.check_build() + # self.builder.check_build() try: result = self.builder[self.word] result = result.strip().replace('\r\n', '<br />')\ @@ -788,7 +793,7 @@ class QueryResult(MapDict): def __init__(self, *args, **kwargs): super(QueryResult, self).__init__(*args, **kwargs) # avoid return None - if self['result'] == None: + if self['result'] is None: self['result'] = "" def set_styles(self, **kwargs): From ec5d49bd97f0324cdcdbd425ec0b9b0f796daa95 Mon Sep 17 00:00:00 2001 From: Javan Zhu <zhujavan@qq.com> Date: Fri, 22 Feb 2019 20:51:04 +0800 Subject: [PATCH 5/6] Update download user agent to improve successful rate of downloading mp3 file. --- addons21/fastwq/service/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons21/fastwq/service/base.py b/addons21/fastwq/service/base.py index e593308..bcdac12 100644 --- a/addons21/fastwq/service/base.py +++ b/addons21/fastwq/service/base.py @@ -370,7 +370,7 @@ class WebService(Service): be added onto the stream returned. This is helpful for some web services that sometimes return MP3s that `mplayer` clips early. """ - DEFAULT_UA = 'Mozilla/5.0' + DEFAULT_UA = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36' DEFAULT_TIMEOUT = 3 PADDING = '\0' * 2**11 From ffde6695219cbe7d71531ecc7e8da1e22cd87e47 Mon Sep 17 00:00:00 2001 From: Javan Zhu <zhujavan@qq.com> Date: Fri, 22 Feb 2019 20:59:28 +0800 Subject: [PATCH 6/6] Add pos to Cambridge. --- addons21/fastwq/service/dict/cambridge.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/addons21/fastwq/service/dict/cambridge.py b/addons21/fastwq/service/dict/cambridge.py index 4a1a38d..393d828 100644 --- a/addons21/fastwq/service/dict/cambridge.py +++ b/addons21/fastwq/service/dict/cambridge.py @@ -51,6 +51,9 @@ class Cambridge(WebService): if snd: result['pronunciation'][pn+'mp3'] = cambridge_url_base + snd.get('data-src-mp3') header_found = True + # 词性 + pg = element.find('span', class_='posgram ico-bg') + #义 body = element.find('div', class_='pos-body') if body: @@ -63,9 +66,11 @@ class Cambridge(WebService): trans = tag.find('span', class_='trans') es = tag.find_all('div', class_='examp emphasized') l.append( - u'<li>{0}{1}{2}{3}</li>'.format( + u'<li>{0}{1}{2} {3}{4}</li>'.format( + '<span class="epp-xref">{0}</span>'.format(pg.get_text() if pg else ''), u'<span class="epp-xref">{0}</span>'.format(i.get_text()) if i else u'', u'<b class="def">{0}</b>'.format(d.get_text()) if d else u'', + u'<span class="trans">{0}</span>'.format(trans.get_text()) if trans else u'', u''.join( u'<div class="examp">{0}</div>'.format(e.get_text()) if e else u''