diff --git a/bin/srv.py b/bin/srv.py index 40270d0..0d850f4 100644 --- a/bin/srv.py +++ b/bin/srv.py @@ -247,7 +247,11 @@ def answer(topic=None): return "429 %s\n" % not_allowed, 429 html_is_needed = is_html_needed(user_agent) and not is_result_a_script(topic) - result, found = cheat_wrapper(topic, request_options=options, html=html_is_needed) + if html_is_needed: + output_format='html' + else: + output_format='ansi' + result, found = cheat_wrapper(topic, request_options=options, output_format=output_format) if 'Please come back in several hours' in result and html_is_needed: return MALFORMED_RESPONSE_HTML_PAGE diff --git a/lib/adapter/adapter.py b/lib/adapter/adapter.py index 0143740..b3d7671 100644 --- a/lib/adapter/adapter.py +++ b/lib/adapter/adapter.py @@ -1,19 +1,50 @@ import abc class Adapter(object): + + _adapter_name = None + _output_format = 'code' + def __init__(self): - self._list = self._get_list() + self._list = {None: self._get_list()} @abc.abstractmethod - def _get_list(self): + def _get_list(self, prefix=None): return [] - def get_list(self): - return self._list + def get_list(self, prefix=None): + """ + Return available pages for `prefix` + """ + + if prefix in self._list: + return self._list[prefix] + + self._list[prefix] = self._get_list(prefix=prefix) + return self._list[prefix] def is_found(self, topic): - return topic in self._list + """ + check if `topic` is available + CAUTION: only root is checked + """ + return topic in self._list[None] @abc.abstractmethod - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): + """ + Return page for `topic` + """ pass + + def get_page_dict(self, topic, request_options=None): + """ + Return page dict for `topic` + """ + answer_dict = { + 'topic': topic, + 'topic_type': self._adapter_name, + 'answer': self._get_page(topic, request_options=request_options), + 'format': self._output_format, + } + return answer_dict diff --git a/lib/adapter/cheat_sheets.py b/lib/adapter/cheat_sheets.py index be14f3d..7db3b5f 100644 --- a/lib/adapter/cheat_sheets.py +++ b/lib/adapter/cheat_sheets.py @@ -5,6 +5,8 @@ import glob sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from globals import PATH_CHEAT_SHEETS +from adapter import Adapter + def _remove_initial_underscore(filename): if filename.startswith('_'): filename = filename[1:] @@ -31,41 +33,48 @@ def _get_answers_and_dirs(): answers = [os.path.split(topic)[1] for topic in topics if not _isdir(topic)] return answers, answer_dirs -def _update_cheat_sheets_topics(): - answers = _get_answer_files_from_folder() - cheatsheet_answers, cheatsheet_dirs = _get_answers_and_dirs() - return answers+cheatsheet_answers, cheatsheet_dirs +class CheatSheets(Adapter): -def get_list(): - return _update_cheat_sheets_topics()[0] + _adapter_name = "cheat.sheets" + _output_format = "code" + + def __init__(self): + self._answers = [] + self._cheatsheet_answers = [] + self._cheatsheet_dirs = [] + Adapter.__init__(self) -def get_dirs_list(): - return _update_cheat_sheets_topics()[1] + def _update_cheat_sheets_topics(self): + self._answers = _get_answer_files_from_folder() + self._cheatsheet_answers, self._cheatsheet_dirs = _get_answers_and_dirs() + return self._answers + self._cheatsheet_answers, self._cheatsheet_dirs -_CHEAT_SHEETS_LIST = get_list() -def is_found(topic): - return topic in _CHEAT_SHEETS_LIST + def _get_list(self, prefix=None): + return self._update_cheat_sheets_topics()[0] -_CHEAT_SHEETS_DIRS = get_dirs_list() -def is_dir_found(topic): - return topic in _CHEAT_SHEETS_DIRS - -def get_page(topic, request_options=None): - """ - Get the cheat sheet topic from the own repository (cheat.sheets). - It's possible that topic directory starts with omitted underscore - """ - filename = PATH_CHEAT_SHEETS + "%s" % topic - if not os.path.exists(filename): - filename = PATH_CHEAT_SHEETS + "_%s" % topic - if os.path.isdir(filename): - return "" - else: + def _get_page(self, topic, request_options=None): + """ + Get the cheat sheet topic from the own repository (cheat.sheets). + It's possible that topic directory starts with omitted underscore + """ + filename = PATH_CHEAT_SHEETS + "%s" % topic + if not os.path.exists(filename): + filename = PATH_CHEAT_SHEETS + "_%s" % topic + if os.path.isdir(filename): + return "" return open(filename, "r").read().decode('utf-8') -def get_dir(topic, request_options=None): - answer = [] - for f_name in glob.glob(PATH_CHEAT_SHEETS + "%s/*" % topic.rstrip('/')): - answer.append(os.path.basename(f_name)) - topics = sorted(answer) - return "\n".join(topics) + "\n" +class CheatSheetsDir(CheatSheets): + + _adapter_name = "cheat.sheets dir" + _output_format = "text" + + def _get_list(self, prefix=None): + return self._update_cheat_sheets_topics()[1] + + def _get_page(self, topic, request_options=None): + answer = [] + for f_name in glob.glob(PATH_CHEAT_SHEETS + "%s/*" % topic.rstrip('/')): + answer.append(os.path.basename(f_name)) + topics = sorted(answer) + return "\n".join(topics) + "\n" diff --git a/lib/adapter/cmd.py b/lib/adapter/cmd.py index a47a49b..906e7b3 100644 --- a/lib/adapter/cmd.py +++ b/lib/adapter/cmd.py @@ -15,11 +15,15 @@ def _get_filenames(path): return [os.path.split(topic)[1] for topic in glob.glob(path)] class Tldr(Adapter): - def _get_list(self): + + _adapter_name = "tldr" + _output_format = "code" + + def _get_list(self, prefix=None): return [filename[:-3] for filename in _get_filenames(PATH_TLDR_PAGES) if filename.endswith('.md')] - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): cmd = ["tldr", topic] proc = Popen(cmd, stdout=PIPE, stderr=PIPE) answer = proc.communicate()[0] @@ -40,30 +44,42 @@ class Tldr(Adapter): return answer.decode('utf-8') class Cheat(Adapter): - def _get_list(self): + + _adapter_name = "cheat" + _output_format = "code" + + def _get_list(self, prefix=None): return _get_filenames(PATH_CHEAT_PAGES) - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): cmd = ["cheat", topic] proc = Popen(cmd, stdout=PIPE, stderr=PIPE) answer = proc.communicate()[0].decode('utf-8') return answer class Fosdem(Adapter): - def _get_list(self): + + _adapter_name = "fosdem" + _output_format = "ansi" + + def _get_list(self, prefix=None): return ['fosdem'] - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): cmd = ["sudo", "/usr/local/bin/current-fosdem-slide"] proc = Popen(cmd, stdout=PIPE, stderr=PIPE) answer = proc.communicate()[0].decode('utf-8') return answer class Translation(Adapter): - def _get_list(self): + + _adapter_name = "translation" + _output_format = "text" + + def _get_list(self, prefix=None): return [] - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): from_, topic = topic.split('/', 1) to_ = request_options.get('lang', 'en') if '-' in from_: diff --git a/lib/adapter/internal.py b/lib/adapter/internal.py index 3b3dc37..ca99f3e 100644 --- a/lib/adapter/internal.py +++ b/lib/adapter/internal.py @@ -7,7 +7,8 @@ from fuzzywuzzy import process, fuzz sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from globals import MYDIR, COLOR_STYLES -from colorize_internal import colorize_internal +from adapter import Adapter +from fmt.internal import colorize_internal _INTERNAL_TOPICS = [ ":cht.sh", @@ -33,9 +34,13 @@ _COLORIZED_INTERNAL_TOPICS = [ ':intro', ] -class InternalPages(object): +class InternalPages(Adapter): + + _adapter_name = 'internal' + _output_format = 'ansi' def __init__(self, get_topic_type=None, get_topics_list=None): + Adapter.__init__(self) self.get_topic_type = get_topic_type self.get_topics_list = get_topics_list @@ -50,9 +55,8 @@ class InternalPages(object): answer += "%s %s\n" % (key, val) return answer - @staticmethod - def get_list(): + def get_list(prefix=None): return _INTERNAL_TOPICS def _get_list_answer(self, topic, request_options=None): @@ -70,7 +74,7 @@ class InternalPages(object): return answer - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): if topic.endswith('/:list') or topic.lstrip('/') == ':list': return self._get_list_answer(topic) @@ -91,15 +95,18 @@ class InternalPages(object): class UnknownPages(InternalPages): + _adapter_name = 'unknown' + _output_format = 'text' + @staticmethod - def get_list(): + def get_list(prefix=None): return [] @staticmethod def is_found(topic): return False - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): topics_list = self.get_topics_list() if topic.startswith(':'): topics_list = [x for x in topics_list if x.startswith(':')] diff --git a/lib/adapter/latenz.py b/lib/adapter/latenz.py index 02358db..f9a25b0 100644 --- a/lib/adapter/latenz.py +++ b/lib/adapter/latenz.py @@ -1,16 +1,21 @@ import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - from globals import PATH_LATENZ +from adapter import Adapter -def get_answer(topic, request_options=None): - sys.path.append(PATH_LATENZ) - import latencies - return latencies.render() +class Latenz(Adapter): -def get_list(): - return ['latencies'] + _adapter_name = "late.nz" + _output_format = "ansi" -def is_found(topic): - return topic.lower() in ['latencies', 'late.nz', 'latency'] + def _get_page(self, topic, request_options=None): + sys.path.append(PATH_LATENZ) + import latencies + return latencies.render() + + def _get_list(self, prefix=None): + return ['latencies'] + + def is_found(self, topic): + return topic.lower() in ['latencies', 'late.nz', 'latency'] diff --git a/lib/adapter/learnxiny.py b/lib/adapter/learnxiny.py index 1924998..a50747e 100644 --- a/lib/adapter/learnxiny.py +++ b/lib/adapter/learnxiny.py @@ -7,6 +7,8 @@ import os import re from globals import PATH_LEARNXINY +from adapter import Adapter + class LearnXYAdapter(object): """ @@ -23,7 +25,6 @@ class LearnXYAdapter(object): _block_cut_end = 0 def __init__(self): - self._whole_cheatsheet = self._read_cheatsheet() self._blocks = self._extract_blocks() @@ -111,7 +112,7 @@ class LearnXYAdapter(object): return True return False - def get_list(self, prefix=False): + def get_list(self, prefix=None): """ Get list of topics for `prefix` """ @@ -119,7 +120,7 @@ class LearnXYAdapter(object): return ["%s/%s" % (self.prefix, x) for x in self._topics_list] return self._topics_list - def get_cheat_sheet(self, name, partial=False): + def get_page(self, name, partial=False): """ Return specified cheat sheet `name` for the language. If `partial`, cheat sheet name may be shortened @@ -142,9 +143,6 @@ class LearnXYAdapter(object): for block_name, block_contents in self._blocks: if block_name == name: - - print("\n".join(block_contents)) - print(name) return "\n".join(block_contents) return None @@ -792,34 +790,40 @@ class LearnVisualBasicAdapter(LearnXYAdapter): _filename = "visualbasic.html.markdown" _splitted = False -ADAPTERS = {cls.prefix: cls() for cls in vars()['LearnXYAdapter'].__subclasses__()} +_ADAPTERS = {cls.prefix: cls() for cls in vars()['LearnXYAdapter'].__subclasses__()} -def get_learnxiny(topic, request_options=None): - """ - Return cheat sheet for `topic` - or empty string if nothing found - """ - lang, topic = topic.split('/', 1) - if lang not in ADAPTERS: - return '' - return ADAPTERS[lang].get_cheat_sheet(topic) +class LearnXinY(Adapter): -def get_learnxiny_list(): - """ - Return list of all learnxiny topics - """ - answer = [] - for language_adapter in ADAPTERS.values(): - answer += language_adapter.get_list(prefix=True) - return answer + def __init__(self): + self.adapters = _ADAPTERS + Adapter.__init__(self) -def is_valid_learnxy(topic): - """ - Return whether `topic` is a valid learnxiny topic - """ + def _get_page(self, topic, request_options=None): + """ + Return cheat sheet for `topic` + or empty string if nothing found + """ + lang, topic = topic.split('/', 1) + if lang not in self.adapters: + return '' + return self.adapters[lang].get_page(topic) - lang, topic = topic.split('/', 1) - if lang not in ADAPTERS: - return False + def _get_list(self, prefix=None): + """ + Return list of all learnxiny topics + """ + answer = [] + for language_adapter in self.adapters.values(): + answer += language_adapter.get_list(prefix=True) + return answer - return ADAPTERS[lang].is_valid(topic) + def is_found(self, topic): + """ + Return whether `topic` is a valid learnxiny topic + """ + + lang, topic = topic.split('/', 1) + if lang not in self.adapters: + return False + + return self.adapters[lang].is_valid(topic) diff --git a/lib/adapter/question.py b/lib/adapter/question.py index 4473358..22dd84e 100644 --- a/lib/adapter/question.py +++ b/lib/adapter/question.py @@ -12,52 +12,65 @@ from polyglot.detect.base import UnknownLanguage sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from globals import MYDIR -def get_page(topic, request_options=None): - """ - Find answer for the `topic` question. - """ +from adapter import Adapter - # if there is a language name in the section name, - # cut it off (de:python => python) - if '/' in topic: - section_name, topic = topic.split('/', 1) - if ':' in section_name: - _, section_name = section_name.split(':', 1) - topic = "%s/%s" % (section_name, topic) +class Question(Adapter): - # some clients send queries with - instead of + so we have to rewrite them to - topic = re.sub(r"(? python) + if '/' in topic: + section_name, topic = topic.split('/', 1) + if ':' in section_name: + _, section_name = section_name.split(':', 1) + topic = "%s/%s" % (section_name, topic) - lang = 'en' - try: - query_text = topic # " ".join(topic) - query_text = re.sub('^[^/]*/+', '', query_text.rstrip('/')) - query_text = re.sub('/[0-9]+$', '', query_text) - query_text = re.sub('/[0-9]+$', '', query_text) - detector = Detector(query_text) - supposed_lang = detector.languages[0].code - if len(topic_words) > 2 or supposed_lang in ['az', 'ru', 'uk', 'de', 'fr', 'es', 'it', 'nl']: - lang = supposed_lang - if supposed_lang.startswith('zh_') or supposed_lang == 'zh': - lang = 'zh' - elif supposed_lang.startswith('pt_'): - lang = 'pt' - if supposed_lang in ['ja', 'ko']: - lang = supposed_lang + # some clients send queries with - instead of + so we have to rewrite them to + topic = re.sub(r"(? 2 or supposed_lang in ['az', 'ru', 'uk', 'de', 'fr', 'es', 'it', 'nl']: + lang = supposed_lang + if supposed_lang.startswith('zh_') or supposed_lang == 'zh': + lang = 'zh' + elif supposed_lang.startswith('pt_'): + lang = 'pt' + if supposed_lang in ['ja', 'ko']: + lang = supposed_lang + + except UnknownLanguage: + print("Unknown language (%s)" % query_text) + + if lang != 'en': + topic = ['--human-language', lang, topic] + else: + topic = [topic] + + cmd = [os.path.join(MYDIR, "bin/get-answer-for-question")] + topic + proc = Popen(cmd, stdout=PIPE, stderr=PIPE) + answer = proc.communicate()[0].decode('utf-8') + return answer + + def get_list(self, prefix=None): + return [] + + def is_found(self, topic): + return True diff --git a/lib/adapter/rosetta.py b/lib/adapter/rosetta.py index 8bdcc17..67c7a26 100644 --- a/lib/adapter/rosetta.py +++ b/lib/adapter/rosetta.py @@ -24,6 +24,8 @@ class Rosetta(Adapter): """ __section_name = 'rosetta' + _adapter_name = "rosetta" + _output_format = "code" @staticmethod def _load_rosetta_code_names(): @@ -101,7 +103,7 @@ class Rosetta(Adapter): ) % number_of_pages return answer - def get_page(self, topic, request_options=None): + def _get_page(self, topic, request_options=None): if '/' not in topic: return self._rosetta_get_list(topic) @@ -118,11 +120,10 @@ class Rosetta(Adapter): return self._get_task(lang, topic) - def _get_list(self): + def _get_list(self, prefix=None): return [] - def get_list(self): - # return self._get_list() + def get_list(self, prefix=None): answer = [self.__section_name] for i in self._rosetta_code_name: answer.append('%s/%s/' % (i, self.__section_name)) diff --git a/lib/cheat_wrapper.py b/lib/cheat_wrapper.py index 40d6b62..02932e4 100644 --- a/lib/cheat_wrapper.py +++ b/lib/cheat_wrapper.py @@ -1,274 +1,30 @@ """ Main cheat.sh wrapper. -Get answers from getters (in get_answer), adds syntax highlighting -or html markup and returns the result. +Parse the query, get answers from getters (using get_answer), +visualize it using frontends and return the result. + +Exports: + + cheat_wrapper() """ -from gevent.monkey import patch_all -from gevent.subprocess import Popen, PIPE -patch_all() - -# pylint: disable=wrong-import-position,wrong-import-order -import sys -import os import re +import json -import colored -from pygments import highlight as pygments_highlight -from pygments.formatters import Terminal256Formatter # pylint: disable=no-name-in-module +from get_answer import get_answer, find_answer_by_keyword, get_topics_list +import frontend.html +import frontend.ansi -MYDIR = os.path.abspath(os.path.join(__file__, '..', '..')) -sys.path.append("%s/lib/" % MYDIR) -from globals import error, ANSI2HTML, COLOR_STYLES, GITHUB_REPOSITORY -from buttons import TWITTER_BUTTON, GITHUB_BUTTON, GITHUB_BUTTON_FOOTER -from languages_data import LEXER, get_lexer_name -from get_answer import get_topic_type, get_topics_list, get_answer, find_answer_by_keyword -from beautifier import code_blocks -# import beautifier -# pylint: disable=wrong-import-position,wrong-import-order - -ANSI_ESCAPE = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') -def remove_ansi(sometext): +def cheat_wrapper(query, request_options=None, output_format='ansi'): """ - Remove ANSI sequences from `sometext` and convert it into plaintext. - """ - return ANSI_ESCAPE.sub('', sometext) - -def html_wrapper(data): - """ - Convert ANSI text `data` to HTML - """ - proc = Popen( - ["bash", ANSI2HTML, "--palette=solarized", "--bg=dark"], - stdin=PIPE, stdout=PIPE, stderr=PIPE) - data = data.encode('utf-8') - stdout, stderr = proc.communicate(data) - if proc.returncode != 0: - error(stdout + stderr) - return stdout.decode('utf-8') - -def _colorize_internal(topic, answer, html_needed): - - def _colorize_line(line): - if line.startswith('T'): - line = colored.fg("grey_62") + line + colored.attr('reset') - line = re.sub(r"\{(.*?)\}", colored.fg("orange_3") + r"\1"+colored.fg('grey_35'), line) - return line - - line = re.sub(r"\[(F.*?)\]", - colored.bg("black") + colored.fg("cyan") + r"[\1]"+colored.attr('reset'), - line) - line = re.sub(r"\[(g.*?)\]", - colored.bg("dark_gray") \ - + colored.fg("grey_0") \ - + r"[\1]"+colored.attr('reset'), - line) - line = re.sub(r"\{(.*?)\}", - colored.fg("orange_3") + r"\1"+colored.attr('reset'), - line) - line = re.sub(r"<(.*?)>", - colored.fg("cyan") + r"\1"+colored.attr('reset'), - line) - return line - - if topic in [':list', ':bash_completion']: - return answer - - if topic == ':firstpage-v1': - lines = answer.splitlines() - answer_lines = lines[:9] - answer_lines.append(colored.fg('grey_35')+lines[9]+colored.attr('reset')) - for line in lines[10:]: - answer_lines.append(_colorize_line(line)) - if html_needed: - answer_lines = answer_lines[:-2] - answer = "\n".join(answer_lines) + "\n" - - return answer - -def _colorize_ansi_answer(topic, answer, color_style, # pylint: disable=too-many-arguments - highlight_all=True, highlight_code=False, - unindent_code=False): - - color_style = color_style or "native" - lexer_class = LEXER['bash'] - if '/' in topic: - section_name = topic.split('/', 1)[0].lower() - section_name = get_lexer_name(section_name) - lexer_class = LEXER.get(section_name, lexer_class) - if section_name == 'php': - answer = "\n%s?>\n" % answer - - if highlight_all: - highlight = lambda answer: pygments_highlight( - answer, lexer_class(), Terminal256Formatter(style=color_style)).strip('\n')+'\n' - else: - highlight = lambda x: x - - if highlight_code: - blocks = code_blocks(answer, wrap_lines=True, unindent_code=(4 if unindent_code else False)) - highlighted_blocks = [] - for block in blocks: - if block[0] == 1: - this_block = highlight(block[1]) - else: - this_block = block[1].strip('\n')+'\n' - highlighted_blocks.append(this_block) - - result = "\n".join(highlighted_blocks) - else: - result = highlight(answer).lstrip('\n') - return result - -def _github_button(topic_type): - - full_name = GITHUB_REPOSITORY.get(topic_type, '') - if not full_name: - return '' - - short_name = full_name.split('/', 1)[1] # pylint: disable=unused-variable - - button = ( - "" - '%(short_name)s' - ) % locals() - return button - -def _render_html(query, result, editable, repository_button, request_options): - - result = result + "\n$" - result = html_wrapper(result) - title = "
' - '[edit]' - '') % edit_page_link - result = re.sub("
", edit_button + form_html + "", result)
- result = re.sub("", "" + title, result)
- if not request_options.get('quiet'):
- result = result.replace('',
- TWITTER_BUTTON \
- + GITHUB_BUTTON \
- + repository_button \
- + GITHUB_BUTTON_FOOTER \
- + '')
- return result
-
-def _visualize(query, keyword, answers, request_options, html=None): # pylint: disable=too-many-locals
-
- search_mode = bool(keyword)
-
- highlight = not bool(request_options and request_options.get('no-terminal'))
- color_style = request_options.get('style', '')
- if color_style not in COLOR_STYLES:
- color_style = ''
-
- found = True # if the page was found in the database
- editable = False # can generated page be edited on github (only cheat.sheets pages can)
- result = ""
- for topic, answer in answers: # pylint: disable=too-many-nested-blocks
-
- if topic == 'LIMITED':
- result += colored.bg('dark_goldenrod') \
- + colored.fg('yellow_1') \
- + ' ' + answer + ' ' \
- + colored.attr('reset') + "\n"
- break
-
- topic_type = get_topic_type(topic)
- highlight = (highlight
- and not topic.endswith('/:list')
- and topic not in [":list", ":bash_completion"]
- and topic_type not in ["unknown"]
- )
- found = found and not topic_type == 'unknown'
- editable = editable or topic_type == "cheat.sheets"
-
- if topic_type == "internal" and highlight:
- answer = _colorize_internal(topic, answer, html)
- elif topic_type in ["late.nz", "fosdem"]:
- pass
- else:
- answer = _colorize_ansi_answer(
- topic, answer, color_style,
- highlight_all=highlight,
- highlight_code=(topic_type == 'question'
- and not request_options.get('add_comments')
- and not request_options.get('remove_text')),
- unindent_code=request_options.get('unindent_code')
- )
-
- if search_mode:
- if not highlight:
- result += "\n[%s]\n" % topic
- else:
- result += "\n%s%s %s %s%s\n" % (colored.bg('dark_gray'),
- colored.attr("res_underlined"),
- topic,
- colored.attr("res_underlined"),
- colored.attr('reset'))
- result += answer
-
- result = result.strip('\n') + "\n"
-
- if search_mode:
- editable = False
- repository_button = ''
- else:
- repository_button = _github_button(topic_type)
-
- if html and query:
- result = _render_html(
- query, result, editable, repository_button, request_options)
-
-
- return result, found
-
-def _sanitize_query(query):
- return re.sub('[<>"]', '', query)
-
-def cheat_wrapper(query, request_options=None, html=False):
- """
- Giant megafunction that delivers cheat sheet for `query`.
+ Function that delivers cheat sheet for `query`.
If `html` is True, the answer is formatted as HTML.
Additional request options specified in `request_options`.
-
- This function is really really bad, and should be rewritten
- as soon as possible.
"""
+ def _sanitize_query(query):
+ return re.sub('[<>"]', '', query)
+
def _strip_hyperlink(query):
return re.sub('(,[0-9]+)+$', '', query)
@@ -302,6 +58,17 @@ def cheat_wrapper(query, request_options=None, html=False):
answers = find_answer_by_keyword(
topic, keyword, options=search_options, request_options=request_options)
else:
- answers = [(topic, get_answer(topic, keyword, request_options=request_options))]
+ answers = [get_answer(topic, keyword, request_options=request_options)]
- return _visualize(query, keyword, answers, request_options, html=html)
+ answer_data = {
+ 'query': query,
+ 'keyword': keyword,
+ 'answers': answers,
+ }
+
+ if output_format == 'html':
+ answer_data['topics_list'] = get_topics_list()
+ return frontend.html.visualize(answer_data, request_options)
+ elif output_format == 'json':
+ return json.dumps(answer_data, indent=4)
+ return frontend.ansi.visualize(answer_data, request_options)
diff --git a/lib/fmt/__init__.py b/lib/fmt/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib/colorize_internal.py b/lib/fmt/internal.py
similarity index 65%
rename from lib/colorize_internal.py
rename to lib/fmt/internal.py
index 2d709ac..a30ab95 100644
--- a/lib/colorize_internal.py
+++ b/lib/fmt/internal.py
@@ -93,3 +93,38 @@ def colorize_internal(text, palette_number=1):
text = re.sub("{.*?}", _colorize_curlies_block, text)
text = re.sub("#(.*?)\n", _colorize_headers, text)
return text
+
+def colorize_internal_firstpage_v1(answer):
+ """
+ Colorize "/:firstpage-v1".
+ Legacy.
+ """
+
+ def _colorize_line(line):
+ if line.startswith('T'):
+ line = colored.fg("grey_62") + line + colored.attr('reset')
+ line = re.sub(r"\{(.*?)\}", colored.fg("orange_3") + r"\1"+colored.fg('grey_35'), line)
+ return line
+
+ line = re.sub(r"\[(F.*?)\]",
+ colored.bg("black") + colored.fg("cyan") + r"[\1]"+colored.attr('reset'),
+ line)
+ line = re.sub(r"\[(g.*?)\]",
+ colored.bg("dark_gray")+colored.fg("grey_0")+r"[\1]"+colored.attr('reset'),
+ line)
+ line = re.sub(r"\{(.*?)\}",
+ colored.fg("orange_3") + r"\1"+colored.attr('reset'),
+ line)
+ line = re.sub(r"<(.*?)>",
+ colored.fg("cyan") + r"\1"+colored.attr('reset'),
+ line)
+ return line
+
+ lines = answer.splitlines()
+ answer_lines = lines[:9]
+ answer_lines.append(colored.fg('grey_35')+lines[9]+colored.attr('reset'))
+ for line in lines[10:]:
+ answer_lines.append(_colorize_line(line))
+ answer = "\n".join(answer_lines) + "\n"
+
+ return answer
diff --git a/lib/fmt/markdown.py b/lib/fmt/markdown.py
new file mode 100644
index 0000000..e120a8c
--- /dev/null
+++ b/lib/fmt/markdown.py
@@ -0,0 +1,89 @@
+"""
+Markdown support.
+
+Exports:
+ format_text(text, config=None, highlighter=None):
+
+Uses external pygments formatters for highlighting (passed as an argument).
+"""
+
+import re
+import ansiwrap
+import colored
+
+def format_text(text, config=None, highlighter=None):
+ """
+ Renders `text` according to markdown rules.
+ Uses `highlighter` for syntax highlighting.
+ Returns a dictionary with "output" and "links".
+ """
+ return _format_section(text, config=config, highlighter=highlighter)
+
+def _split_into_paragraphs(text):
+ return re.split('\n\n+', text)
+
+def _colorize(text):
+ return \
+ re.sub(
+ r"`(.*?)`",
+ colored.bg("dark_gray") \
+ + colored.fg("white") \
+ + " " + r"\1" + " " \
+ + colored.attr('reset'),
+ re.sub(
+ r"\*\*(.*?)\*\*",
+ colored.attr('bold') \
+ + colored.fg("white") \
+ + r"\1" \
+ + colored.attr('reset'),
+ text))
+
+def _format_section(section_text, config=None, highlighter=None):
+
+ answer = ''
+
+ # cut code blocks
+ block_number = 0
+ while True:
+ section_text, replacements = re.subn(
+ '^```.*?^```',
+ 'MULTILINE_BLOCK_%s' % block_number,
+ section_text,
+ 1,
+ flags=re.S | re.MULTILINE)
+ block_number += 1
+ if not replacements:
+ break
+
+ # cut links
+ links = []
+ while True:
+ regexp = re.compile(r'\[(.*?)\]\((.*?)\)')
+ match = regexp.search(section_text)
+ if match:
+ links.append(match.group(0))
+ text = match.group(1)
+ # links are not yet supported
+ #
+ text = '\x1B]8;;%s\x1B\\\\%s\x1B]8;;\x1B\\\\' % (match.group(2), match.group(1))
+ else:
+ break
+
+
+ section_text, replacements = regexp.subn(
+ text, # 'LINK_%s' % len(links),
+ section_text,
+ 1)
+ block_number += 1
+ if not replacements:
+ break
+
+ for paragraph in _split_into_paragraphs(section_text):
+ answer += "\n".join(
+ ansiwrap.fill(_colorize(line)) + "\n"
+ for line in paragraph.splitlines()) + "\n"
+
+ return {
+ 'ansi': answer,
+ 'links': links
+ }
diff --git a/lib/frontend/__init__.py b/lib/frontend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib/frontend/ansi.py b/lib/frontend/ansi.py
new file mode 100644
index 0000000..755587f
--- /dev/null
+++ b/lib/frontend/ansi.py
@@ -0,0 +1,129 @@
+"""
+ANSI frontend.
+
+Exports:
+ visualize(answer_data, request_options)
+
+Format:
+ answer_data = {
+ 'answers': '...',}
+
+ answers = [answer,...]
+
+ answer = {
+ 'topic': '...',
+ 'topic_type': '...',
+ 'answer': '...',
+ 'format': 'ansi|code|markdown|text...',
+ }
+"""
+
+import os
+import sys
+import re
+
+import colored
+from pygments import highlight as pygments_highlight
+from pygments.formatters import Terminal256Formatter # pylint: disable=no-name-in-module
+ # pylint: disable=wrong-import-position
+sys.path.append(os.path.abspath(os.path.join(__file__, '..')))
+from globals import COLOR_STYLES
+import languages_data
+import beautifier # pylint: enable=wrong-import-position
+
+import fmt.internal
+
+def visualize(answer_data, request_options):
+ """
+ Renders `answer_data` as ANSI output.
+ """
+ answers = answer_data['answers']
+ return _visualize(answers, request_options, search_mode=bool(answer_data['keyword']))
+
+ANSI_ESCAPE = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
+def remove_ansi(sometext):
+ """
+ Remove ANSI sequences from `sometext` and convert it into plaintext.
+ """
+ return ANSI_ESCAPE.sub('', sometext)
+
+def _limited_answer(answer):
+ return colored.bg('dark_goldenrod') + colored.fg('yellow_1') \
+ + ' ' + answer + ' ' \
+ + colored.attr('reset') + "\n"
+
+def _colorize_ansi_answer(topic, answer, color_style, # pylint: disable=too-many-arguments
+ highlight_all=True, highlight_code=False,
+ unindent_code=False):
+
+ color_style = color_style or "native"
+ lexer_class = languages_data.LEXER['bash']
+ if '/' in topic:
+ section_name = topic.split('/', 1)[0].lower()
+ section_name = languages_data.get_lexer_name(section_name)
+ lexer_class = languages_data.LEXER.get(section_name, lexer_class)
+ if section_name == 'php':
+ answer = "\n%s?>\n" % answer
+
+ if highlight_all:
+ highlight = lambda answer: pygments_highlight(
+ answer, lexer_class(), Terminal256Formatter(style=color_style)).strip('\n')+'\n'
+ else:
+ highlight = lambda x: x
+
+ if highlight_code:
+ blocks = beautifier.code_blocks(
+ answer, wrap_lines=True, unindent_code=(4 if unindent_code else False))
+ highlighted_blocks = []
+ for block in blocks:
+ if block[0] == 1:
+ this_block = highlight(block[1])
+ else:
+ this_block = block[1].strip('\n')+'\n'
+ highlighted_blocks.append(this_block)
+
+ result = "\n".join(highlighted_blocks)
+ else:
+ result = highlight(answer).lstrip('\n')
+ return result
+
+def _visualize(answers, request_options, search_mode=False):
+
+ highlight = not bool(request_options and request_options.get('no-terminal'))
+ color_style = request_options.get('style', '')
+ if color_style not in COLOR_STYLES:
+ color_style = ''
+
+ found = True
+ result = ""
+ for answer_dict in answers:
+ topic = answer_dict['topic']
+ topic_type = answer_dict['topic_type']
+ answer = answer_dict['answer']
+ found = found and not topic_type == 'unknown'
+
+ if answer_dict['format'] in ['ansi', 'text']:
+ result += answer
+ elif topic == ':firstpage-v1':
+ result += fmt.internal.colorize_internal_firstpage_v1(answer)
+ elif topic == 'LIMITED':
+ result += _limited_answer(answer)
+ else:
+ result += _colorize_ansi_answer(
+ topic, answer, color_style,
+ highlight_all=highlight,
+ highlight_code=(topic_type == 'question'
+ and not request_options.get('add_comments')
+ and not request_options.get('remove_text')))
+
+ if search_mode:
+ if not highlight:
+ result += "\n[%s]\n" % topic
+ else:
+ result += "\n%s%s %s %s%s\n" % (
+ colored.bg('dark_gray'), colored.attr("res_underlined"),
+ topic,
+ colored.attr("res_underlined"), colored.attr('reset'))
+
+ result = result.strip('\n') + "\n"
+ return result, found
diff --git a/lib/frontend/html.py b/lib/frontend/html.py
new file mode 100644
index 0000000..168db91
--- /dev/null
+++ b/lib/frontend/html.py
@@ -0,0 +1,106 @@
+from gevent.monkey import patch_all
+from gevent.subprocess import Popen, PIPE
+patch_all()
+
+# pylint: disable=wrong-import-position,wrong-import-order
+import sys
+import os
+import re
+
+MYDIR = os.path.abspath(os.path.join(__file__, '..', '..'))
+sys.path.append("%s/lib/" % MYDIR)
+
+from globals import error, ANSI2HTML, GITHUB_REPOSITORY
+from buttons import TWITTER_BUTTON, GITHUB_BUTTON, GITHUB_BUTTON_FOOTER
+import frontend.ansi
+# pylint: disable=wrong-import-position,wrong-import-order
+
+def visualize(answer_data, request_options):
+ query = answer_data['query']
+ answers = answer_data['answers']
+ topics_list = answer_data['topics_list']
+ editable = (len(answers) == 1 and answers[0]['topic_type'] == 'cheat.sheets')
+
+ repository_button = ''
+ if len(answers) == 1:
+ repository_button = _github_button(answers[0]['topic_type'])
+
+ result = frontend.ansi.visualize(answer_data, request_options)
+ return _render_html(query, result, editable, repository_button, topics_list, request_options)
+
+def _github_button(topic_type):
+
+ full_name = GITHUB_REPOSITORY.get(topic_type, '')
+ if not full_name:
+ return ''
+
+ short_name = full_name.split('/', 1)[1] # pylint: disable=unused-variable
+
+ button = (
+ ""
+ '%(short_name)s'
+ ) % locals()
+ return button
+
+def _render_html(query, result, editable, repository_button, topics_list, request_options):
+
+ def _html_wrapper(data):
+ """
+ Convert ANSI text `data` to HTML
+ """
+ proc = Popen(
+ ["bash", ANSI2HTML, "--palette=solarized", "--bg=dark"],
+ stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ data = data.encode('utf-8')
+ stdout, stderr = proc.communicate(data)
+ if proc.returncode != 0:
+ error(stdout + stderr)
+ return stdout.decode('utf-8')
+
+
+ result = result + "\n$"
+ result = _html_wrapper(result)
+ title = "cheat.sh/%s " % query
+ submit_button = ('')
+ topic_list = (''
+ % ("\n".join("" % x for x in topics_list)))
+
+ curl_line = "$ curl cheat.sh/"
+ if query == ':firstpage':
+ query = ""
+ form_html = (''
+ '%s%s'
+ ''
+ '%s'
+ '') \
+ % (submit_button, curl_line, query, topic_list)
+
+ edit_button = ''
+ if editable:
+ # It's possible that topic directory starts with omitted underscore
+ if '/' in query:
+ query = '_' + query
+ edit_page_link = 'https://github.com/chubin/cheat.sheets/edit/master/sheets/' + query
+ edit_button = (
+ '' + '[edit]' + '') % edit_page_link + result = re.sub("
", edit_button + form_html + "", result)
+ result = re.sub("", "" + title, result)
+ if not request_options.get('quiet'):
+ result = result.replace('',
+ TWITTER_BUTTON \
+ + GITHUB_BUTTON \
+ + repository_button \
+ + GITHUB_BUTTON_FOOTER \
+ + '')
+ return result
diff --git a/lib/get_answer.py b/lib/get_answer.py
index ffeb5ca..ce42782 100644
--- a/lib/get_answer.py
+++ b/lib/get_answer.py
@@ -20,11 +20,11 @@ from languages_data import LANGUAGE_ALIAS, SO_NAME, rewrite_editor_section_name
import adapter.cheat_sheets
import adapter.cmd
-import adapter.latenz
-import adapter.question
import adapter.internal
-import adapter.rosetta
+import adapter.latenz
import adapter.learnxiny
+import adapter.question
+import adapter.rosetta
class Router(object):
@@ -61,42 +61,39 @@ class Router(object):
"fosdem": adapter.cmd.Fosdem(),
"translation": adapter.cmd.Translation(),
"rosetta": adapter.rosetta.Rosetta(),
+ "late.nz": adapter.latenz.Latenz(),
+ "question": adapter.question.Question(),
+ "cheat.sheets": adapter.cheat_sheets.CheatSheets(),
+ "cheat.sheets dir": adapter.cheat_sheets.CheatSheetsDir(),
+ "learnxiny": adapter.learnxiny.LearnXinY(),
}
self._topic_list = {
- "late.nz": adapter.latenz.get_list(),
- "cheat.sheets": adapter.cheat_sheets.get_list(),
- "cheat.sheets dir": adapter.cheat_sheets.get_dirs_list(),
- "learnxiny": adapter.learnxiny.get_learnxiny_list(),
+ key: obj.get_list()
+ for key, obj in self._adapter.items()
}
- for key, obj in self._adapter.items():
- self._topic_list[key] = obj.get_list()
self._topic_found = {
- "late.nz": adapter.latenz.is_found,
- "cheat.sheets": adapter.cheat_sheets.is_found,
- "cheat.sheets dir": adapter.cheat_sheets.is_dir_found,
- "learnxiny": adapter.learnxiny.is_valid_learnxy,
+ key: obj.is_found
+ for key, obj in self._adapter.items()
}
- for key, obj in self._adapter.items():
- self._topic_found[key] = obj.is_found
# topic_type, function_getter
# should be replaced with a decorator
# pylint: disable=bad-whitespace
self.topic_getters = (
- ("late.nz", adapter.latenz.get_answer),
- ("cheat.sheets", adapter.cheat_sheets.get_page),
- ("cheat.sheets dir", adapter.cheat_sheets.get_dir),
- ("learnxiny", adapter.learnxiny.get_learnxiny),
- ("question", adapter.question.get_page),
- ("fosdem", self._adapter["fosdem"].get_page),
- ("rosetta", self._adapter["rosetta"].get_page),
- ("tldr", self._adapter["tldr"].get_page),
- ("internal", self._adapter["internal"].get_page),
- ("cheat", self._adapter["cheat"].get_page),
- ("translation", self._adapter["translation"].get_page),
- ("unknown", self._adapter["unknown"].get_page),
+ ("cheat.sheets", self._adapter["cheat.sheets"].get_page_dict),
+ ("cheat.sheets dir", self._adapter["cheat.sheets dir"].get_page_dict),
+ ("learnxiny", self._adapter["learnxiny"].get_page_dict),
+ ("question", self._adapter["question"].get_page_dict),
+ ("fosdem", self._adapter["fosdem"].get_page_dict),
+ ("late.nz", self._adapter["late.nz"].get_page_dict),
+ ("rosetta", self._adapter["rosetta"].get_page_dict),
+ ("tldr", self._adapter["tldr"].get_page_dict),
+ ("internal", self._adapter["internal"].get_page_dict),
+ ("cheat", self._adapter["cheat"].get_page_dict),
+ ("translation", self._adapter["translation"].get_page_dict),
+ ("unknown", self._adapter["unknown"].get_page_dict),
)
# pylint: enable=bad-whitespace
@@ -120,10 +117,10 @@ class Router(object):
answer.update({name:key for name in self._topic_list[key]})
answer = sorted(set(answer.keys()))
- # doing it in this strange way to save the order of the topics
- for topic in adapter.learnxiny.get_learnxiny_list():
- if topic not in answer:
- answer.append(topic)
+ # # doing it in this strange way to save the order of the topics
+ # for topic in adapter.learnxiny.get_learnxiny_list():
+ # if topic not in answer:
+ # answer.append(topic)
self._cached_topics_list = answer
return answer
@@ -159,7 +156,7 @@ class Router(object):
# topic contains '/'
#
- if adapter.learnxiny.is_valid_learnxy(topic):
+ if self._adapter['learnxiny'].is_found(topic):
return 'learnxiny'
topic_type = topic.split('/', 1)[0]
if topic_type in ['ru', 'fr'] or re.match(r'[a-z][a-z]-[a-z][a-z]$', topic_type):
@@ -337,10 +334,20 @@ def get_answer(topic, keyword, options="", request_options=None): # pylint: disa
if filetype.startswith('q:'):
filetype = filetype[2:]
- answer = beautifier.beautify(answer.encode('utf-8'), filetype, request_options)
+ answer['answer'] = beautifier.beautify(answer['answer'].encode('utf-8'), filetype, request_options)
+
+ # if isinstance(answer, str):
+ # answer_dict = {
+ # 'topic': topic,
+ # 'topic_type': topic_type,
+ # 'answer': answer,
+ # 'format': 'code',
+ # }
+ # else:
+ answer_dict = answer
if not keyword:
- return answer
+ return answer_dict
#
# shorten the answer, because keyword is specified
@@ -357,6 +364,7 @@ def get_answer(topic, keyword, options="", request_options=None): # pylint: disa
return ""
answer = _join_paragraphs(paragraphs)
+
return answer
def find_answer_by_keyword(directory, keyword, options="", request_options=None):
@@ -379,10 +387,13 @@ def find_answer_by_keyword(directory, keyword, options="", request_options=None)
answer = get_answer(topic, keyword, options=options, request_options=request_options)
if answer:
- answer_paragraphs.append((topic, answer))
+ answer_paragraphs.append(answer)
if len(answer_paragraphs) > MAX_SEARCH_LEN:
- answer_paragraphs.append(("LIMITED", "LIMITED TO %s ANSWERS" % MAX_SEARCH_LEN))
+ answer_paragraphs.append({
+ 'topic_type': 'LIMITED',
+ 'answer': "LIMITED TO %s ANSWERS" % MAX_SEARCH_LEN,
+ })
break
return answer_paragraphs