Source code for crate_anon.crateweb.research.html_functions

#!/usr/bin/env python
# crate_anon/crateweb/research/html_functions.py

"""
===============================================================================

    Copyright (C) 2015-2018 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of CRATE.

    CRATE is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CRATE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CRATE. If not, see <http://www.gnu.org/licenses/>.

===============================================================================
"""

import logging
import re
import textwrap
from typing import Any, Dict, Iterable, List, Optional, Pattern

from cardinal_pythonlib.django.function_cache import django_cache_function
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.utils.html import escape
# from django.template import loader
from django.template.defaultfilters import linebreaksbr
from pygments import highlight
from pygments.lexers.sql import SqlLexer
from pygments.formatters.html import HtmlFormatter
import sqlparse

log = logging.getLogger(__name__)


N_CSS_HIGHLIGHT_CLASSES = 3  # named highlight0, highlight1, ... highlight<n-1>
REGEX_METACHARS = ["\\", "^", "$", ".",
                   "|", "?", "*", "+",
                   "(", ")", "[", "{"]
# http://www.regular-expressions.info/characters.html
# Start with \, for replacement.


# =============================================================================
# Collapsible div, etc.
# =============================================================================

def visibility_button(tag: str, small: bool = True,
                      title_html: str = '', as_span: bool = False,
                      as_visibility: bool = True) -> str:
    eltype = "span" if as_span else "div"
    togglefunc = "toggleVisible" if as_visibility else "toggleCollapsed"
    return """
<{eltype} class="expandcollapse" onclick="{togglefunc}('collapsible_{tag}', 'collapse_img_{tag}');">
    <img class="plusminus_image" id="collapse_img_{tag}" alt="" src="{img}">
    {title_html}
</{eltype}>
    """.format(  # noqa
        eltype=eltype,
        togglefunc=togglefunc,
        tag=str(tag),
        img=static('plus.gif') if small else static('minus.gif'),
        title_html=title_html,
    )


def visibility_contentdiv(tag: str,
                          contents: str,
                          extra_div_classes: Iterable[str] = None,
                          small: bool = True,
                          as_visibility: bool = True) -> str:
    extra_div_classes = extra_div_classes or []
    div_classes = ["collapsible"] + extra_div_classes
    if as_visibility:
        if small:
            div_classes.append("collapse_invisible")
        else:
            div_classes.append("collapse_visible")
    else:
        if small:
            div_classes.append("collapse_small")
        else:
            div_classes.append("collapse_big")
    return """
<div class="{div_classes}" id="collapsible_{tag}">
    {contents}
</div>
    """.format(
        div_classes=" ".join(div_classes),
        tag=str(tag),
        contents=contents,
    )


def visibility_div_with_divbutton(tag: str,
                                  contents: str,
                                  title_html: str = '',
                                  extra_div_classes: Iterable[str] = None,
                                  small: bool = True) -> str:
    # The HTML pre-hides, rather than using an onload method
    button = visibility_button(tag=tag, small=small,
                               title_html=title_html, as_visibility=True)
    contents = visibility_contentdiv(tag=tag, contents=contents,
                                     extra_div_classes=extra_div_classes,
                                     small=small, as_visibility=True)
    return "<div>" + button + contents + "</div>"


def overflow_div(tag: str,
                 contents: str,
                 extra_div_classes: Iterable[str] = None,
                 small: bool = True) -> str:
    button = visibility_button(tag=tag, small=small,
                               as_visibility=False)
    contentdiv = visibility_contentdiv(tag=tag, contents=contents,
                                       extra_div_classes=extra_div_classes,
                                       small=small, as_visibility=False)
    return """
<div class="expandcollapsewrapper">
    {button}
    {contentdiv}
</div>
    """.format(button=button, contentdiv=contentdiv)


# =============================================================================
# Class to maintain element counters, for use with pages having lots of
# collapsible divs (or other HTML elements requiring individual numbering)
# =============================================================================

class HtmlElementCounter(object):
    def __init__(self, prefix: str = ''):
        self.elementnum = 0
        self.prefix = prefix

    def next(self):
        self.elementnum += 1

    def tag(self):
        return self.prefix + str(self.elementnum)

    def visibility_div_with_divbutton(self,
                                      contents: str,
                                      title_html: str = '',
                                      extra_div_classes: Iterable[str] = None,
                                      visible: bool = True) -> str:
        result = visibility_div_with_divbutton(
            tag=self.tag(),
            contents=contents,
            title_html=title_html,
            extra_div_classes=extra_div_classes,
            small=visible)
        self.next()
        return result

    def visibility_div_spanbutton(self, small: bool = True) -> str:
        return visibility_button(tag=self.tag(), as_visibility=True,
                                 small=small, as_span=True)

    def visibility_div_contentdiv(self,
                                  contents: str,
                                  extra_div_classes: Iterable[str] = None,
                                  small: bool = True) -> str:
        result = visibility_contentdiv(
            tag=self.tag(),
            contents=contents,
            extra_div_classes=extra_div_classes,
            small=small,
            as_visibility=True)
        self.next()
        return result

    def collapsible_div_contentdiv(self,
                                   contents: str,
                                   extra_div_classes: Iterable[str] = None,
                                   small: bool = True) -> str:
        result = visibility_contentdiv(
            tag=self.tag(),
            contents=contents,
            extra_div_classes=extra_div_classes,
            small=small,
            as_visibility=False)
        self.next()
        return result

    def overflow_div(self,
                     contents: str,
                     extradivclasses: Iterable[str] = None,
                     small: bool = True) -> str:
        result = overflow_div(tag=self.tag(),
                              contents=contents,
                              extra_div_classes=extradivclasses,
                              small=small)
        self.next()
        return result


# =============================================================================
# Highlighting of query results
# =============================================================================

HIGHLIGHT_FWD_REF = "Highlight"


[docs]def escape_literal_string_for_regex(s: str) -> str: r""" Escape any regex characters. Start with \ -> \\ ... this should be the first replacement in REGEX_METACHARS. """ for c in REGEX_METACHARS: s.replace(c, "\\" + c) return s
def get_regex_from_highlights(highlight_list: Iterable[HIGHLIGHT_FWD_REF], at_word_boundaries_only: bool = False) \ -> Pattern: elements = [] wb = r"\b" # word boundary; escape the slash if not using a raw string for hl in highlight_list: h = escape_literal_string_for_regex(hl.text) if at_word_boundaries_only: elements.append(wb + h + wb) else: elements.append(h) regexstring = u"(" + "|".join(elements) + ")" # group required, to replace return re.compile(regexstring, re.IGNORECASE | re.UNICODE) def highlight_text(x: str, n: int = 0) -> str: n %= N_CSS_HIGHLIGHT_CLASSES return r'<span class="highlight{n}">{x}</span>'.format(n=n, x=x) def make_highlight_replacement_regex(n: int = 0) -> str: return highlight_text(r"\1", n=n) def make_result_element(x: Optional[str], element_counter: HtmlElementCounter, highlight_dict: Dict[int, List[HIGHLIGHT_FWD_REF]] = None, collapse_at_len: int = None, collapse_at_n_lines: int = None, line_length: int = None, keep_existing_newlines: bool = True, collapsed: bool = True, null: str = '<i>NULL</i>') -> str: # return escape(repr(x)) if x is None: return null highlight_dict = highlight_dict or {} x = str(x) xlen = len(x) # before we mess around with it # textwrap.wrap will absorb existing newlines if keep_existing_newlines: input_lines = x.split("\n") else: input_lines = [x] if line_length: output_lines = [] for line in input_lines: if line: output_lines.extend(textwrap.wrap(line, width=line_length)) else: # blank line; textwrap.wrap will swallow it output_lines.append('') else: output_lines = input_lines n_lines = len(output_lines) # return escape(repr(output_lines)) output = linebreaksbr(escape("\n".join(output_lines))) # return escape(repr(output)) for n, highlight_list in highlight_dict.items(): find = get_regex_from_highlights(highlight_list) replace = make_highlight_replacement_regex(n) output = find.sub(replace, output) if ((collapse_at_len and xlen >= collapse_at_len) or (collapse_at_n_lines and n_lines >= collapse_at_n_lines)): result = element_counter.overflow_div(contents=output, small=collapsed) element_counter.next() else: result = output return result def pre(x: str = '') -> str: return "<pre>{}</pre>".format(x) # ============================================================================= # SQL formatting # ============================================================================= SQL_BASE_CSS_CLASS = "sq" # brief is good SQL_FORMATTER = HtmlFormatter(cssclass=SQL_BASE_CSS_CLASS) SQL_LEXER = SqlLexer() def prettify_sql_html(sql: str, reformat: bool = False, indent_width: int = 4) -> str: if reformat: sql = sqlparse.format(sql, reindent=True, indent_width=indent_width) return highlight(sql, SQL_LEXER, SQL_FORMATTER) @django_cache_function(timeout=None) def prettify_sql_css() -> str: return SQL_FORMATTER.get_style_defs() def prettify_sql_and_args(sql: str, args: List[Any] = None, reformat: bool = False, indent_width: int = 4) -> str: sql = prettify_sql_html(sql, reformat=reformat, indent_width=indent_width) if args: formatted_args = "\n".join(textwrap.wrap(repr(args))) return sql + "<div>Args:</div><pre>{}</pre>".format(formatted_args) else: return sql def make_collapsible_sql_query(x: Optional[str], element_counter: HtmlElementCounter, args: List[Any] = None, collapse_at_len: int = 400, collapse_at_n_lines: int = 5) -> str: x = x or '' x = str(x) xlen = len(x) n_lines = len(x.split('\n')) formatted = prettify_sql_and_args(x, args, reformat=False) # x = linebreaksbr(escape(x)) if ((collapse_at_len and xlen >= collapse_at_len) or (collapse_at_n_lines and n_lines >= collapse_at_n_lines)): return element_counter.overflow_div(contents=formatted) return formatted