1
1
mirror of https://github.com/chubin/cheat.sh.git synced 2024-11-23 10:35:29 +03:00

cheat_wrapper.py splitted into fmt/ and frontend/

This commit is contained in:
Igor Chubin 2019-02-16 20:11:34 +01:00
parent 775cf0005c
commit c59bdab2f2
17 changed files with 664 additions and 437 deletions

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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_:

View File

@ -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(':')]

View File

@ -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']

View File

@ -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)

View File

@ -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"(?<!-)-", ' ', topic)
_adapter_name = "question"
_output_format = "code"
topic_words = topic.split()
def _get_page(self, topic, request_options=None):
"""
Find answer for the `topic` question.
"""
topic = " ".join(topic_words)
# 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)
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"(?<!-)-", ' ', topic)
except UnknownLanguage:
print("Unknown language (%s)" % query_text)
topic_words = topic.split()
if lang != 'en':
topic = ['--human-language', lang, topic]
else:
topic = [topic]
topic = " ".join(topic_words)
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
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
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

View File

@ -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))

View File

@ -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 = (
"<!-- Place this tag where you want the button to render. -->"
'<a aria-label="Star %(full_name)s on GitHub"'
' data-count-aria-label="# stargazers on GitHub"'
' data-count-api="/repos/%(full_name)s#stargazers_count"'
' data-count-href="/%(full_name)s/stargazers"'
' data-icon="octicon-star"'
' href="https://github.com/%(full_name)s"'
' class="github-button">%(short_name)s</a>'
) % locals()
return button
def _render_html(query, result, editable, repository_button, request_options):
result = result + "\n$"
result = html_wrapper(result)
title = "<title>cheat.sh/%s</title>" % query
# title += ('\n<link rel="stylesheet" href="/files/awesomplete.css" />script'
# ' src="/files/awesomplete.min.js" async></script>')
# submit button: thanks to http://stackoverflow.com/questions/477691/
submit_button = ('<input type="submit" style="position: absolute;'
' left: -9999px; width: 1px; height: 1px;" tabindex="-1" />')
topic_list = ('<datalist id="topics">%s</datalist>'
% ("\n".join("<option value='%s'></option>" % x for x in get_topics_list())))
curl_line = "<span class='pre'>$ curl cheat.sh/</span>"
if query == ':firstpage':
query = ""
form_html = ('<form action="/" method="GET"/>'
'%s%s'
'<input'
' type="text" value="%s" name="topic"'
' list="topics" autofocus autocomplete="off"/>'
'%s'
'</form>') \
% (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 = (
'<pre style="position:absolute;padding-left:40em;overflow:visible;height:0;">'
'[<a href="%s" style="color:cyan">edit</a>]'
'</pre>') % edit_page_link
result = re.sub("<pre>", edit_button + form_html + "<pre>", result)
result = re.sub("<head>", "<head>" + title, result)
if not request_options.get('quiet'):
result = result.replace('</body>',
TWITTER_BUTTON \
+ GITHUB_BUTTON \
+ repository_button \
+ GITHUB_BUTTON_FOOTER \
+ '</body>')
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)

0
lib/fmt/__init__.py Normal file
View File

View File

@ -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

89
lib/fmt/markdown.py Normal file
View File

@ -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
}

0
lib/frontend/__init__.py Normal file
View File

129
lib/frontend/ansi.py Normal file
View File

@ -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

106
lib/frontend/html.py Normal file
View File

@ -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 = (
"<!-- Place this tag where you want the button to render. -->"
'<a aria-label="Star %(full_name)s on GitHub"'
' data-count-aria-label="# stargazers on GitHub"'
' data-count-api="/repos/%(full_name)s#stargazers_count"'
' data-count-href="/%(full_name)s/stargazers"'
' data-icon="octicon-star"'
' href="https://github.com/%(full_name)s"'
' class="github-button">%(short_name)s</a>'
) % 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 = "<title>cheat.sh/%s</title>" % query
submit_button = ('<input type="submit" style="position: absolute;'
' left: -9999px; width: 1px; height: 1px;" tabindex="-1" />')
topic_list = ('<datalist id="topics">%s</datalist>'
% ("\n".join("<option value='%s'></option>" % x for x in topics_list)))
curl_line = "<span class='pre'>$ curl cheat.sh/</span>"
if query == ':firstpage':
query = ""
form_html = ('<form action="/" method="GET"/>'
'%s%s'
'<input'
' type="text" value="%s" name="topic"'
' list="topics" autofocus autocomplete="off"/>'
'%s'
'</form>') \
% (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 = (
'<pre style="position:absolute;padding-left:40em;overflow:visible;height:0;">'
'[<a href="%s" style="color:cyan">edit</a>]'
'</pre>') % edit_page_link
result = re.sub("<pre>", edit_button + form_html + "<pre>", result)
result = re.sub("<head>", "<head>" + title, result)
if not request_options.get('quiet'):
result = result.replace('</body>',
TWITTER_BUTTON \
+ GITHUB_BUTTON \
+ repository_button \
+ GITHUB_BUTTON_FOOTER \
+ '</body>')
return result

View File

@ -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