Replace pecan's homegrown interactive debugging middleware with backlash
backlash is a port of Werkzeug's debugger middleware to Webob. It has no additional dependencies beyond Webob and is being used by the TurboGears2 team as an alternative to the antiquated Paste/WebError. Leveraging this as an *optional* dependency to pecan would: * Remove a sizable chunk of code from pecan, some of which is embedded JavaScript that packagers have traditionally balked at. * Improve the interactive debugging experience for developers in a very meaningful way (the Werkzeug-based middleware provides features like an in-browser console debugger, the ability to load source code on a frame-by-frame basis). * Improve the unified debugging experience amongst several popular Python frameworks (some form of the debugging interface will be in use by Flask, Pecan, and TurboGears2). Change-Id: I85f50f677c6052bd2afd32811dedf33835135e12
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.6 KiB |
@@ -21,37 +21,18 @@ in your applications. To enable the debugging middleware, simply set the
|
||||
|
||||
Once enabled, the middleware will automatically catch exceptions raised by your
|
||||
application and display the Python stack trace and WSGI environment in your
|
||||
browser for easy debugging:
|
||||
browser when runtime exceptions are raised.
|
||||
|
||||
.. figure:: debug-middleware-1.png
|
||||
:alt: Pecan debug middleware sample output.
|
||||
:width: 90%
|
||||
To improve debugging, including support for an interactive browser-based
|
||||
console, Pecan makes use of the Python `backlash
|
||||
<https://pypi.python.org/pypi/backlash>` library. You’ll need to install it
|
||||
for development use before continuing::
|
||||
|
||||
To further aid in debugging, the middleware includes the ability to repeat the
|
||||
offending request, automatically inserting a breakpoint, and dropping your
|
||||
console into the Python debugger, ``pdb.post_mortem``:
|
||||
$ pip install backlash
|
||||
Downloading/unpacking backlash
|
||||
...
|
||||
Successfully installed backlash
|
||||
|
||||
.. figure:: debug-middleware-2.png
|
||||
:alt: Pecan debug middleware request debugger.
|
||||
|
||||
You can also use any debugger with a suitable ``post_mortem`` entry point.
|
||||
For example, to use the `PuDB Debugger <http://pypi.python.org/pypi/pudb>`_,
|
||||
set ``debugger`` like so::
|
||||
|
||||
import pudb
|
||||
|
||||
app = {
|
||||
...
|
||||
'debug': True,
|
||||
'debugger': pudb.post_mortem,
|
||||
...
|
||||
}
|
||||
|
||||
.. seealso::
|
||||
|
||||
Refer to the `pdb documentation
|
||||
<http://docs.python.org/library/pdb.html>`_ for more information on
|
||||
using the Python debugger.
|
||||
|
||||
Serving Static Files
|
||||
--------------------
|
||||
|
||||
@@ -20,7 +20,6 @@ try:
|
||||
except ImportError:
|
||||
from logutils.dictconfig import dictConfig as load_logging_config # noqa
|
||||
|
||||
import six
|
||||
import warnings
|
||||
|
||||
|
||||
@@ -44,8 +43,6 @@ def make_app(root, **kw):
|
||||
debug mode is set.
|
||||
:param debug: A flag to enable debug mode. This enables the debug
|
||||
middleware and serving static files.
|
||||
:param debugger: A callable to start debugging, defaulting to the Python
|
||||
debugger entry point ``pdb.post_mortem``.
|
||||
:param wrap_app: A function or middleware class to wrap the Pecan app.
|
||||
This must either be a wsgi middleware class or a
|
||||
function that returns a wsgi application. This wrapper
|
||||
@@ -101,19 +98,19 @@ def make_app(root, **kw):
|
||||
# Included for internal redirect support
|
||||
app = middleware.recursive.RecursiveMiddleware(app)
|
||||
|
||||
# When in debug mode, load our exception dumping middleware
|
||||
# When in debug mode, load exception debugging middleware
|
||||
static_root = kw.get('static_root', None)
|
||||
if debug:
|
||||
debugger = kw.get('debugger', None)
|
||||
debugger_kwargs = {}
|
||||
if six.callable(debugger):
|
||||
debugger_kwargs['debugger'] = debugger
|
||||
elif debugger:
|
||||
warnings.warn(
|
||||
"`app.debugger` is not callable, ignoring",
|
||||
RuntimeWarning
|
||||
)
|
||||
app = middleware.debug.DebugMiddleware(app, **debugger_kwargs)
|
||||
debug_kwargs = getattr(conf, 'debug', {})
|
||||
debug_kwargs.setdefault('context_injectors', []).append(
|
||||
lambda environ: {
|
||||
'request': environ.get('pecan.locals', {}).get('request')
|
||||
}
|
||||
)
|
||||
app = DebugMiddleware(
|
||||
app,
|
||||
**debug_kwargs
|
||||
)
|
||||
|
||||
# Support for serving static files (for development convenience)
|
||||
if static_root:
|
||||
|
||||
@@ -660,6 +660,10 @@ class PecanBase(object):
|
||||
req = self.request_cls(environ)
|
||||
resp = self.response_cls()
|
||||
state = RoutingState(req, resp, self)
|
||||
environ['pecan.locals'] = {
|
||||
'request': req,
|
||||
'response': resp
|
||||
}
|
||||
controller = None
|
||||
|
||||
# handle the request
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from . import debug
|
||||
from . import errordocument
|
||||
from . import recursive
|
||||
from . import static
|
||||
|
||||
@@ -1,324 +1,96 @@
|
||||
from traceback import print_exc
|
||||
from pprint import pformat
|
||||
import pdb
|
||||
__CONFIG_HELP__ = '''
|
||||
<div class="traceback">
|
||||
<b>To disable this interface, set </b>
|
||||
<a target="window"
|
||||
href="https://pecan.readthedocs.org/en/latest/deployment.html#disabling-debug-mode">
|
||||
<pre>conf.app.debug = False</pre>
|
||||
</a>
|
||||
</div>
|
||||
''' # noqa
|
||||
|
||||
from six.moves import cStringIO as StringIO
|
||||
try:
|
||||
import re
|
||||
from backlash.debug import DebuggedApplication
|
||||
|
||||
from mako.template import Template
|
||||
from webob import Response
|
||||
class DebugMiddleware(DebuggedApplication):
|
||||
|
||||
from .resources import (pecan_image, xregexp_js, syntax_js, syntax_css, theme,
|
||||
brush)
|
||||
body_re = re.compile('(<body[^>]*>)', re.I)
|
||||
|
||||
def debug_application(self, environ, start_response):
|
||||
for part in super(DebugMiddleware, self).debug_application(
|
||||
environ, start_response
|
||||
):
|
||||
yield self.body_re.sub('\g<1>%s' % __CONFIG_HELP__, part)
|
||||
|
||||
|
||||
debug_template_raw = '''<html>
|
||||
<head>
|
||||
<title>Pecan - Application Error</title>
|
||||
except ImportError:
|
||||
from traceback import print_exc
|
||||
from pprint import pformat
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="${syntax_css}" />
|
||||
<link rel="stylesheet" type="text/css" href="${theme}" />
|
||||
from mako.template import Template
|
||||
from six.moves import cStringIO as StringIO
|
||||
from webob import Response
|
||||
from webob.exc import HTTPException
|
||||
|
||||
<script type="text/javascript" src="${xregexp_js}"></script>
|
||||
<script type="text/javascript" src="${syntax_js}">
|
||||
/**
|
||||
* SyntaxHighlighter
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter
|
||||
*
|
||||
* SyntaxHighlighter is donationware. If you are using it, please donate.
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
|
||||
*
|
||||
* @version
|
||||
* 3.0.83 (July 02 2010)
|
||||
*
|
||||
* @copyright
|
||||
* Copyright (C) 2004-2010 Alex Gorbatchev.
|
||||
*
|
||||
* @license
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
</script>
|
||||
<script type="text/javascript" src="${brush}">
|
||||
/**
|
||||
* SyntaxHighlighter
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter
|
||||
*
|
||||
* SyntaxHighlighter is donationware. If you are using it, please donate.
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
|
||||
*
|
||||
* @version
|
||||
* 3.0.83 (July 02 2010)
|
||||
*
|
||||
* @copyright
|
||||
* Copyright (C) 2004-2010 Alex Gorbatchev.
|
||||
*
|
||||
* @license
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
</script>
|
||||
debug_template_raw = '''<html>
|
||||
<head>
|
||||
<title>Pecan - Application Error</title>
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
An error occurred!
|
||||
</h1>
|
||||
</header>
|
||||
<div id="error-content">
|
||||
<p>
|
||||
%(config_help)s
|
||||
Pecan offers support for interactive debugging by installing the <a href="https://pypi.python.org/pypi/backlash" target="window">backlash</a> package:
|
||||
<br />
|
||||
<b><pre>pip install backlash</pre></b>
|
||||
...and reloading this page.
|
||||
</p>
|
||||
<h2>Traceback</h2>
|
||||
<div id="traceback">
|
||||
<pre>${traceback}</pre>
|
||||
</div>
|
||||
<h2>WSGI Environment</h2>
|
||||
<div id="environ">
|
||||
<pre>${environment}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''' % {'config_help': __CONFIG_HELP__} # noqa
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
color: #000;
|
||||
background: #FFF;
|
||||
font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
debug_template = Template(debug_template_raw)
|
||||
|
||||
a {
|
||||
color: #FAFF78;
|
||||
}
|
||||
class DebugMiddleware(object):
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Helvetica', sans-serif;
|
||||
}
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
self.app = app
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: .75em 1.5em 1em 1.5em;
|
||||
color: #F90;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
def __call__(self, environ, start_response):
|
||||
try:
|
||||
return self.app(environ, start_response)
|
||||
except Exception as exc:
|
||||
# get a formatted exception
|
||||
out = StringIO()
|
||||
print_exc(file=out)
|
||||
|
||||
h1 img {
|
||||
padding-right: 5px;
|
||||
}
|
||||
# get formatted WSGI environment
|
||||
formatted_environ = pformat(environ)
|
||||
|
||||
h2 {
|
||||
color: #311F00;
|
||||
}
|
||||
# render our template
|
||||
result = debug_template.render(
|
||||
traceback=out.getvalue(),
|
||||
environment=formatted_environ
|
||||
)
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
background: #311F00;
|
||||
}
|
||||
|
||||
div#error-content {
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
.syntaxhighlighter a,
|
||||
.syntaxhighlighter div,
|
||||
.syntaxhighlighter code,
|
||||
.syntaxhighlighter table,
|
||||
.syntaxhighlighter table td,
|
||||
.syntaxhighlighter table tr,
|
||||
.syntaxhighlighter table tbody,
|
||||
.syntaxhighlighter table thead,
|
||||
.syntaxhighlighter table caption,
|
||||
.syntaxhighlighter textarea {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
.syntaxhighlighter .container {
|
||||
background: #FDF6E3 !important;
|
||||
padding: 1em !important;
|
||||
}
|
||||
|
||||
.syntaxhighlighter .container .line {
|
||||
background: #FDF6E3 !important;
|
||||
}
|
||||
|
||||
.syntaxhighlighter .container .line .python.string {
|
||||
color: #C70 !important;
|
||||
}
|
||||
|
||||
#debug {
|
||||
background: #FDF6E3;
|
||||
padding: 10px !important;
|
||||
margin-top: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
SyntaxHighlighter.defaults['gutter'] = false;
|
||||
SyntaxHighlighter.defaults['toolbar'] = false;
|
||||
SyntaxHighlighter.all()
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
function get_request() {
|
||||
/* ajax sans jquery makes me sad */
|
||||
var request = false;
|
||||
|
||||
// Mozilla/Safari
|
||||
if (window.XMLHttpRequest) {
|
||||
request = new XMLHttpRequest();
|
||||
}
|
||||
|
||||
// IE
|
||||
else if (window.ActiveXObject) {
|
||||
request = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function debug_request(btn) {
|
||||
btn.disabled = true;
|
||||
|
||||
request = get_request();
|
||||
request.open('GET', '/__pecan_initiate_pdb__', true);
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
request.send('');
|
||||
|
||||
/* automatically timeout after 5 minutes, re-enabling the button */
|
||||
setTimeout(function() {
|
||||
request.abort();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
<img style="padding-top: 7px"
|
||||
align="center" alt="pecan logo"
|
||||
height="25"
|
||||
src="${pecan_image}" />
|
||||
application error
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div id="error-content">
|
||||
|
||||
<p>
|
||||
<b>To disable this interface, set </b>
|
||||
<pre class="brush: python">conf.app.debug = False</pre>
|
||||
</p>
|
||||
|
||||
<h2>Traceback</h2>
|
||||
<div id="traceback">
|
||||
<pre class="brush: python">${traceback}</pre>
|
||||
</div>
|
||||
|
||||
% if not debugging:
|
||||
<b>Want to debug this request?</b>
|
||||
<div id="debug">
|
||||
You can <button onclick="debug_request(this)">
|
||||
repeat this request
|
||||
</button> with a Python debugger breakpoint.
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<h2>WSGI Environment</h2>
|
||||
<div id="environ">
|
||||
<pre class="brush: python">${environment}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
debug_template = Template(debug_template_raw)
|
||||
__debug_environ__ = None
|
||||
|
||||
|
||||
class PdbMiddleware(object):
|
||||
def __init__(self, app, debugger):
|
||||
self.app = app
|
||||
self.debugger = debugger
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
try:
|
||||
return self.app(environ, start_response)
|
||||
except:
|
||||
self.debugger()
|
||||
|
||||
|
||||
class DebugMiddleware(object):
|
||||
"""A WSGI middleware that provides debugging assistance for development
|
||||
environments.
|
||||
|
||||
To enable the debugging middleware, simply set the ``debug`` flag to
|
||||
``True`` in your configuration file::
|
||||
|
||||
app = {
|
||||
...
|
||||
'debug': True,
|
||||
...
|
||||
}
|
||||
|
||||
Once enabled, the middleware will automatically catch exceptions raised by
|
||||
your application, and display the Python stack trace and WSGI environment
|
||||
in your browser for easy debugging.
|
||||
|
||||
To further aid in debugging, the middleware includes the ability to repeat
|
||||
the offending request, automatically inserting a breakpoint, and dropping
|
||||
your console into the Python debugger, ``pdb.post_mortem``.
|
||||
|
||||
You can also use any debugger with a suitable ``post_mortem`` entry point
|
||||
such as the `PuDB Debugger <http://pypi.python.org/pypi/pudb>`_,
|
||||
|
||||
For more information, refer to the `documentation for pdb
|
||||
<http://docs.python.org/library/pdb.html>`_ available on the Python
|
||||
website.
|
||||
|
||||
:param app: the application to wrap.
|
||||
:param debugger: a callable to start debugging, defaulting to the Python
|
||||
debugger entry point ``pdb.post_mortem``.
|
||||
"""
|
||||
|
||||
def __init__(self, app, debugger=pdb.post_mortem):
|
||||
self.app = app
|
||||
self.debugger = debugger
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if environ['wsgi.multiprocess']:
|
||||
raise RuntimeError(
|
||||
"The DebugMiddleware middleware is not usable in a "
|
||||
"multi-process environment"
|
||||
)
|
||||
|
||||
if environ.get('paste.testing'):
|
||||
return self.app(environ, start_response)
|
||||
|
||||
# initiate a PDB session if requested
|
||||
global __debug_environ__
|
||||
debugging = environ['PATH_INFO'] == '/__pecan_initiate_pdb__'
|
||||
if debugging:
|
||||
PdbMiddleware(self.app, self.debugger)(
|
||||
__debug_environ__, start_response
|
||||
)
|
||||
environ = __debug_environ__
|
||||
|
||||
try:
|
||||
return self.app(environ, start_response)
|
||||
except:
|
||||
# save the environ for debugging
|
||||
if not debugging:
|
||||
__debug_environ__ = environ
|
||||
|
||||
# get a formatted exception
|
||||
out = StringIO()
|
||||
print_exc(file=out)
|
||||
|
||||
# get formatted WSGI environment
|
||||
formatted_environ = pformat(environ)
|
||||
|
||||
# render our template
|
||||
result = debug_template.render(
|
||||
traceback=out.getvalue(),
|
||||
environment=formatted_environ,
|
||||
pecan_image=pecan_image,
|
||||
xregexp_js=xregexp_js,
|
||||
syntax_js=syntax_js,
|
||||
brush=brush,
|
||||
syntax_css=syntax_css,
|
||||
theme=theme,
|
||||
debugging=debugging
|
||||
)
|
||||
|
||||
# construct and return our response
|
||||
response = Response()
|
||||
response.status_int = 400
|
||||
response.unicode_body = result
|
||||
return response(environ, start_response)
|
||||
# construct and return our response
|
||||
response = Response()
|
||||
if isinstance(exc, HTTPException):
|
||||
response.status_int = exc.status
|
||||
else:
|
||||
response.status_int = 500
|
||||
response.unicode_body = result
|
||||
return response(environ, start_response)
|
||||
|
||||
@@ -1,664 +0,0 @@
|
||||
// XRegExp 1.5.1
|
||||
// (c) 2007-2012 Steven Levithan
|
||||
// MIT License
|
||||
// <http://xregexp.com>
|
||||
// Provides an augmented, extensible, cross-browser implementation of regular expressions,
|
||||
// including support for additional syntax, flags, and methods
|
||||
|
||||
var XRegExp;
|
||||
|
||||
if (XRegExp) {
|
||||
// Avoid running twice, since that would break references to native globals
|
||||
throw Error("can't load XRegExp twice in the same frame");
|
||||
}
|
||||
|
||||
// Run within an anonymous function to protect variables and avoid new globals
|
||||
(function (undefined) {
|
||||
|
||||
//---------------------------------
|
||||
// Constructor
|
||||
//---------------------------------
|
||||
|
||||
// Accepts a pattern and flags; returns a new, extended `RegExp` object. Differs from a native
|
||||
// regular expression in that additional syntax and flags are supported and cross-browser
|
||||
// syntax inconsistencies are ameliorated. `XRegExp(/regex/)` clones an existing regex and
|
||||
// converts to type XRegExp
|
||||
XRegExp = function (pattern, flags) {
|
||||
var output = [],
|
||||
currScope = XRegExp.OUTSIDE_CLASS,
|
||||
pos = 0,
|
||||
context, tokenResult, match, chr, regex;
|
||||
|
||||
if (XRegExp.isRegExp(pattern)) {
|
||||
if (flags !== undefined)
|
||||
throw TypeError("can't supply flags when constructing one RegExp from another");
|
||||
return clone(pattern);
|
||||
}
|
||||
// Tokens become part of the regex construction process, so protect against infinite
|
||||
// recursion when an XRegExp is constructed within a token handler or trigger
|
||||
if (isInsideConstructor)
|
||||
throw Error("can't call the XRegExp constructor within token definition functions");
|
||||
|
||||
flags = flags || "";
|
||||
context = { // `this` object for custom tokens
|
||||
hasNamedCapture: false,
|
||||
captureNames: [],
|
||||
hasFlag: function (flag) {return flags.indexOf(flag) > -1;},
|
||||
setFlag: function (flag) {flags += flag;}
|
||||
};
|
||||
|
||||
while (pos < pattern.length) {
|
||||
// Check for custom tokens at the current position
|
||||
tokenResult = runTokens(pattern, pos, currScope, context);
|
||||
|
||||
if (tokenResult) {
|
||||
output.push(tokenResult.output);
|
||||
pos += (tokenResult.match[0].length || 1);
|
||||
} else {
|
||||
// Check for native multicharacter metasequences (excluding character classes) at
|
||||
// the current position
|
||||
if (match = nativ.exec.call(nativeTokens[currScope], pattern.slice(pos))) {
|
||||
output.push(match[0]);
|
||||
pos += match[0].length;
|
||||
} else {
|
||||
chr = pattern.charAt(pos);
|
||||
if (chr === "[")
|
||||
currScope = XRegExp.INSIDE_CLASS;
|
||||
else if (chr === "]")
|
||||
currScope = XRegExp.OUTSIDE_CLASS;
|
||||
// Advance position one character
|
||||
output.push(chr);
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regex = RegExp(output.join(""), nativ.replace.call(flags, flagClip, ""));
|
||||
regex._xregexp = {
|
||||
source: pattern,
|
||||
captureNames: context.hasNamedCapture ? context.captureNames : null
|
||||
};
|
||||
return regex;
|
||||
};
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Public properties
|
||||
//---------------------------------
|
||||
|
||||
XRegExp.version = "1.5.1";
|
||||
|
||||
// Token scope bitflags
|
||||
XRegExp.INSIDE_CLASS = 1;
|
||||
XRegExp.OUTSIDE_CLASS = 2;
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Private variables
|
||||
//---------------------------------
|
||||
|
||||
var replacementToken = /\$(?:(\d\d?|[$&`'])|{([$\w]+)})/g,
|
||||
flagClip = /[^gimy]+|([\s\S])(?=[\s\S]*\1)/g, // Nonnative and duplicate flags
|
||||
quantifier = /^(?:[?*+]|{\d+(?:,\d*)?})\??/,
|
||||
isInsideConstructor = false,
|
||||
tokens = [],
|
||||
// Copy native globals for reference ("native" is an ES3 reserved keyword)
|
||||
nativ = {
|
||||
exec: RegExp.prototype.exec,
|
||||
test: RegExp.prototype.test,
|
||||
match: String.prototype.match,
|
||||
replace: String.prototype.replace,
|
||||
split: String.prototype.split
|
||||
},
|
||||
compliantExecNpcg = nativ.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups
|
||||
compliantLastIndexIncrement = function () {
|
||||
var x = /^/g;
|
||||
nativ.test.call(x, "");
|
||||
return !x.lastIndex;
|
||||
}(),
|
||||
hasNativeY = RegExp.prototype.sticky !== undefined,
|
||||
nativeTokens = {};
|
||||
|
||||
// `nativeTokens` match native multicharacter metasequences only (including deprecated octals,
|
||||
// excluding character classes)
|
||||
nativeTokens[XRegExp.INSIDE_CLASS] = /^(?:\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|c[A-Za-z]|[\s\S]))/;
|
||||
nativeTokens[XRegExp.OUTSIDE_CLASS] = /^(?:\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\d*|x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|c[A-Za-z]|[\s\S])|\(\?[:=!]|[?*+]\?|{\d+(?:,\d*)?}\??)/;
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Public methods
|
||||
//---------------------------------
|
||||
|
||||
// Lets you extend or change XRegExp syntax and create custom flags. This is used internally by
|
||||
// the XRegExp library and can be used to create XRegExp plugins. This function is intended for
|
||||
// users with advanced knowledge of JavaScript's regular expression syntax and behavior. It can
|
||||
// be disabled by `XRegExp.freezeTokens`
|
||||
XRegExp.addToken = function (regex, handler, scope, trigger) {
|
||||
tokens.push({
|
||||
pattern: clone(regex, "g" + (hasNativeY ? "y" : "")),
|
||||
handler: handler,
|
||||
scope: scope || XRegExp.OUTSIDE_CLASS,
|
||||
trigger: trigger || null
|
||||
});
|
||||
};
|
||||
|
||||
// Accepts a pattern and flags; returns an extended `RegExp` object. If the pattern and flag
|
||||
// combination has previously been cached, the cached copy is returned; otherwise the newly
|
||||
// created regex is cached
|
||||
XRegExp.cache = function (pattern, flags) {
|
||||
var key = pattern + "/" + (flags || "");
|
||||
return XRegExp.cache[key] || (XRegExp.cache[key] = XRegExp(pattern, flags));
|
||||
};
|
||||
|
||||
// Accepts a `RegExp` instance; returns a copy with the `/g` flag set. The copy has a fresh
|
||||
// `lastIndex` (set to zero). If you want to copy a regex without forcing the `global`
|
||||
// property, use `XRegExp(regex)`. Do not use `RegExp(regex)` because it will not preserve
|
||||
// special properties required for named capture
|
||||
XRegExp.copyAsGlobal = function (regex) {
|
||||
return clone(regex, "g");
|
||||
};
|
||||
|
||||
// Accepts a string; returns the string with regex metacharacters escaped. The returned string
|
||||
// can safely be used at any point within a regex to match the provided literal string. Escaped
|
||||
// characters are [ ] { } ( ) * + ? - . , \ ^ $ | # and whitespace
|
||||
XRegExp.escape = function (str) {
|
||||
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||
};
|
||||
|
||||
// Accepts a string to search, regex to search with, position to start the search within the
|
||||
// string (default: 0), and an optional Boolean indicating whether matches must start at-or-
|
||||
// after the position or at the specified position only. This function ignores the `lastIndex`
|
||||
// of the provided regex in its own handling, but updates the property for compatibility
|
||||
XRegExp.execAt = function (str, regex, pos, anchored) {
|
||||
var r2 = clone(regex, "g" + ((anchored && hasNativeY) ? "y" : "")),
|
||||
match;
|
||||
r2.lastIndex = pos = pos || 0;
|
||||
match = r2.exec(str); // Run the altered `exec` (required for `lastIndex` fix, etc.)
|
||||
if (anchored && match && match.index !== pos)
|
||||
match = null;
|
||||
if (regex.global)
|
||||
regex.lastIndex = match ? r2.lastIndex : 0;
|
||||
return match;
|
||||
};
|
||||
|
||||
// Breaks the unrestorable link to XRegExp's private list of tokens, thereby preventing
|
||||
// syntax and flag changes. Should be run after XRegExp and any plugins are loaded
|
||||
XRegExp.freezeTokens = function () {
|
||||
XRegExp.addToken = function () {
|
||||
throw Error("can't run addToken after freezeTokens");
|
||||
};
|
||||
};
|
||||
|
||||
// Accepts any value; returns a Boolean indicating whether the argument is a `RegExp` object.
|
||||
// Note that this is also `true` for regex literals and regexes created by the `XRegExp`
|
||||
// constructor. This works correctly for variables created in another frame, when `instanceof`
|
||||
// and `constructor` checks would fail to work as intended
|
||||
XRegExp.isRegExp = function (o) {
|
||||
return Object.prototype.toString.call(o) === "[object RegExp]";
|
||||
};
|
||||
|
||||
// Executes `callback` once per match within `str`. Provides a simpler and cleaner way to
|
||||
// iterate over regex matches compared to the traditional approaches of subverting
|
||||
// `String.prototype.replace` or repeatedly calling `exec` within a `while` loop
|
||||
XRegExp.iterate = function (str, regex, callback, context) {
|
||||
var r2 = clone(regex, "g"),
|
||||
i = -1, match;
|
||||
while (match = r2.exec(str)) { // Run the altered `exec` (required for `lastIndex` fix, etc.)
|
||||
if (regex.global)
|
||||
regex.lastIndex = r2.lastIndex; // Doing this to follow expectations if `lastIndex` is checked within `callback`
|
||||
callback.call(context, match, ++i, str, regex);
|
||||
if (r2.lastIndex === match.index)
|
||||
r2.lastIndex++;
|
||||
}
|
||||
if (regex.global)
|
||||
regex.lastIndex = 0;
|
||||
};
|
||||
|
||||
// Accepts a string and an array of regexes; returns the result of using each successive regex
|
||||
// to search within the matches of the previous regex. The array of regexes can also contain
|
||||
// objects with `regex` and `backref` properties, in which case the named or numbered back-
|
||||
// references specified are passed forward to the next regex or returned. E.g.:
|
||||
// var xregexpImgFileNames = XRegExp.matchChain(html, [
|
||||
// {regex: /<img\b([^>]+)>/i, backref: 1}, // <img> tag attributes
|
||||
// {regex: XRegExp('(?ix) \\s src=" (?<src> [^"]+ )'), backref: "src"}, // src attribute values
|
||||
// {regex: XRegExp("^http://xregexp\\.com(/[^#?]+)", "i"), backref: 1}, // xregexp.com paths
|
||||
// /[^\/]+$/ // filenames (strip directory paths)
|
||||
// ]);
|
||||
XRegExp.matchChain = function (str, chain) {
|
||||
return function recurseChain (values, level) {
|
||||
var item = chain[level].regex ? chain[level] : {regex: chain[level]},
|
||||
regex = clone(item.regex, "g"),
|
||||
matches = [], i;
|
||||
for (i = 0; i < values.length; i++) {
|
||||
XRegExp.iterate(values[i], regex, function (match) {
|
||||
matches.push(item.backref ? (match[item.backref] || "") : match[0]);
|
||||
});
|
||||
}
|
||||
return ((level === chain.length - 1) || !matches.length) ?
|
||||
matches : recurseChain(matches, level + 1);
|
||||
}([str], 0);
|
||||
};
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// New RegExp prototype methods
|
||||
//---------------------------------
|
||||
|
||||
// Accepts a context object and arguments array; returns the result of calling `exec` with the
|
||||
// first value in the arguments array. the context is ignored but is accepted for congruity
|
||||
// with `Function.prototype.apply`
|
||||
RegExp.prototype.apply = function (context, args) {
|
||||
return this.exec(args[0]);
|
||||
};
|
||||
|
||||
// Accepts a context object and string; returns the result of calling `exec` with the provided
|
||||
// string. the context is ignored but is accepted for congruity with `Function.prototype.call`
|
||||
RegExp.prototype.call = function (context, str) {
|
||||
return this.exec(str);
|
||||
};
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Overriden native methods
|
||||
//---------------------------------
|
||||
|
||||
// Adds named capture support (with backreferences returned as `result.name`), and fixes two
|
||||
// cross-browser issues per ES3:
|
||||
// - Captured values for nonparticipating capturing groups should be returned as `undefined`,
|
||||
// rather than the empty string.
|
||||
// - `lastIndex` should not be incremented after zero-length matches.
|
||||
RegExp.prototype.exec = function (str) {
|
||||
var match, name, r2, origLastIndex;
|
||||
if (!this.global)
|
||||
origLastIndex = this.lastIndex;
|
||||
match = nativ.exec.apply(this, arguments);
|
||||
if (match) {
|
||||
// Fix browsers whose `exec` methods don't consistently return `undefined` for
|
||||
// nonparticipating capturing groups
|
||||
if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) {
|
||||
r2 = RegExp(this.source, nativ.replace.call(getNativeFlags(this), "g", ""));
|
||||
// Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed
|
||||
// matching due to characters outside the match
|
||||
nativ.replace.call((str + "").slice(match.index), r2, function () {
|
||||
for (var i = 1; i < arguments.length - 2; i++) {
|
||||
if (arguments[i] === undefined)
|
||||
match[i] = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Attach named capture properties
|
||||
if (this._xregexp && this._xregexp.captureNames) {
|
||||
for (var i = 1; i < match.length; i++) {
|
||||
name = this._xregexp.captureNames[i - 1];
|
||||
if (name)
|
||||
match[name] = match[i];
|
||||
}
|
||||
}
|
||||
// Fix browsers that increment `lastIndex` after zero-length matches
|
||||
if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index))
|
||||
this.lastIndex--;
|
||||
}
|
||||
if (!this.global)
|
||||
this.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows)
|
||||
return match;
|
||||
};
|
||||
|
||||
// Fix browser bugs in native method
|
||||
RegExp.prototype.test = function (str) {
|
||||
// Use the native `exec` to skip some processing overhead, even though the altered
|
||||
// `exec` would take care of the `lastIndex` fixes
|
||||
var match, origLastIndex;
|
||||
if (!this.global)
|
||||
origLastIndex = this.lastIndex;
|
||||
match = nativ.exec.call(this, str);
|
||||
// Fix browsers that increment `lastIndex` after zero-length matches
|
||||
if (match && !compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index))
|
||||
this.lastIndex--;
|
||||
if (!this.global)
|
||||
this.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows)
|
||||
return !!match;
|
||||
};
|
||||
|
||||
// Adds named capture support and fixes browser bugs in native method
|
||||
String.prototype.match = function (regex) {
|
||||
if (!XRegExp.isRegExp(regex))
|
||||
regex = RegExp(regex); // Native `RegExp`
|
||||
if (regex.global) {
|
||||
var result = nativ.match.apply(this, arguments);
|
||||
regex.lastIndex = 0; // Fix IE bug
|
||||
return result;
|
||||
}
|
||||
return regex.exec(this); // Run the altered `exec`
|
||||
};
|
||||
|
||||
// Adds support for `${n}` tokens for named and numbered backreferences in replacement text,
|
||||
// and provides named backreferences to replacement functions as `arguments[0].name`. Also
|
||||
// fixes cross-browser differences in replacement text syntax when performing a replacement
|
||||
// using a nonregex search value, and the value of replacement regexes' `lastIndex` property
|
||||
// during replacement iterations. Note that this doesn't support SpiderMonkey's proprietary
|
||||
// third (`flags`) parameter
|
||||
String.prototype.replace = function (search, replacement) {
|
||||
var isRegex = XRegExp.isRegExp(search),
|
||||
captureNames, result, str, origLastIndex;
|
||||
|
||||
// There are too many combinations of search/replacement types/values and browser bugs that
|
||||
// preclude passing to native `replace`, so don't try
|
||||
//if (...)
|
||||
// return nativ.replace.apply(this, arguments);
|
||||
|
||||
if (isRegex) {
|
||||
if (search._xregexp)
|
||||
captureNames = search._xregexp.captureNames; // Array or `null`
|
||||
if (!search.global)
|
||||
origLastIndex = search.lastIndex;
|
||||
} else {
|
||||
search = search + ""; // Type conversion
|
||||
}
|
||||
|
||||
if (Object.prototype.toString.call(replacement) === "[object Function]") {
|
||||
result = nativ.replace.call(this + "", search, function () {
|
||||
if (captureNames) {
|
||||
// Change the `arguments[0]` string primitive to a String object which can store properties
|
||||
arguments[0] = new String(arguments[0]);
|
||||
// Store named backreferences on `arguments[0]`
|
||||
for (var i = 0; i < captureNames.length; i++) {
|
||||
if (captureNames[i])
|
||||
arguments[0][captureNames[i]] = arguments[i + 1];
|
||||
}
|
||||
}
|
||||
// Update `lastIndex` before calling `replacement` (fix browsers)
|
||||
if (isRegex && search.global)
|
||||
search.lastIndex = arguments[arguments.length - 2] + arguments[0].length;
|
||||
return replacement.apply(null, arguments);
|
||||
});
|
||||
} else {
|
||||
str = this + ""; // Type conversion, so `args[args.length - 1]` will be a string (given nonstring `this`)
|
||||
result = nativ.replace.call(str, search, function () {
|
||||
var args = arguments; // Keep this function's `arguments` available through closure
|
||||
return nativ.replace.call(replacement + "", replacementToken, function ($0, $1, $2) {
|
||||
// Numbered backreference (without delimiters) or special variable
|
||||
if ($1) {
|
||||
switch ($1) {
|
||||
case "$": return "$";
|
||||
case "&": return args[0];
|
||||
case "`": return args[args.length - 1].slice(0, args[args.length - 2]);
|
||||
case "'": return args[args.length - 1].slice(args[args.length - 2] + args[0].length);
|
||||
// Numbered backreference
|
||||
default:
|
||||
// What does "$10" mean?
|
||||
// - Backreference 10, if 10 or more capturing groups exist
|
||||
// - Backreference 1 followed by "0", if 1-9 capturing groups exist
|
||||
// - Otherwise, it's the string "$10"
|
||||
// Also note:
|
||||
// - Backreferences cannot be more than two digits (enforced by `replacementToken`)
|
||||
// - "$01" is equivalent to "$1" if a capturing group exists, otherwise it's the string "$01"
|
||||
// - There is no "$0" token ("$&" is the entire match)
|
||||
var literalNumbers = "";
|
||||
$1 = +$1; // Type conversion; drop leading zero
|
||||
if (!$1) // `$1` was "0" or "00"
|
||||
return $0;
|
||||
while ($1 > args.length - 3) {
|
||||
literalNumbers = String.prototype.slice.call($1, -1) + literalNumbers;
|
||||
$1 = Math.floor($1 / 10); // Drop the last digit
|
||||
}
|
||||
return ($1 ? args[$1] || "" : "$") + literalNumbers;
|
||||
}
|
||||
// Named backreference or delimited numbered backreference
|
||||
} else {
|
||||
// What does "${n}" mean?
|
||||
// - Backreference to numbered capture n. Two differences from "$n":
|
||||
// - n can be more than two digits
|
||||
// - Backreference 0 is allowed, and is the entire match
|
||||
// - Backreference to named capture n, if it exists and is not a number overridden by numbered capture
|
||||
// - Otherwise, it's the string "${n}"
|
||||
var n = +$2; // Type conversion; drop leading zeros
|
||||
if (n <= args.length - 3)
|
||||
return args[n];
|
||||
n = captureNames ? indexOf(captureNames, $2) : -1;
|
||||
return n > -1 ? args[n + 1] : $0;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (isRegex) {
|
||||
if (search.global)
|
||||
search.lastIndex = 0; // Fix IE, Safari bug (last tested IE 9.0.5, Safari 5.1.2 on Windows)
|
||||
else
|
||||
search.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows)
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// A consistent cross-browser, ES3 compliant `split`
|
||||
String.prototype.split = function (s /* separator */, limit) {
|
||||
// If separator `s` is not a regex, use the native `split`
|
||||
if (!XRegExp.isRegExp(s))
|
||||
return nativ.split.apply(this, arguments);
|
||||
|
||||
var str = this + "", // Type conversion
|
||||
output = [],
|
||||
lastLastIndex = 0,
|
||||
match, lastLength;
|
||||
|
||||
// Behavior for `limit`: if it's...
|
||||
// - `undefined`: No limit
|
||||
// - `NaN` or zero: Return an empty array
|
||||
// - A positive number: Use `Math.floor(limit)`
|
||||
// - A negative number: No limit
|
||||
// - Other: Type-convert, then use the above rules
|
||||
if (limit === undefined || +limit < 0) {
|
||||
limit = Infinity;
|
||||
} else {
|
||||
limit = Math.floor(+limit);
|
||||
if (!limit)
|
||||
return [];
|
||||
}
|
||||
|
||||
// This is required if not `s.global`, and it avoids needing to set `s.lastIndex` to zero
|
||||
// and restore it to its original value when we're done using the regex
|
||||
s = XRegExp.copyAsGlobal(s);
|
||||
|
||||
while (match = s.exec(str)) { // Run the altered `exec` (required for `lastIndex` fix, etc.)
|
||||
if (s.lastIndex > lastLastIndex) {
|
||||
output.push(str.slice(lastLastIndex, match.index));
|
||||
|
||||
if (match.length > 1 && match.index < str.length)
|
||||
Array.prototype.push.apply(output, match.slice(1));
|
||||
|
||||
lastLength = match[0].length;
|
||||
lastLastIndex = s.lastIndex;
|
||||
|
||||
if (output.length >= limit)
|
||||
break;
|
||||
}
|
||||
|
||||
if (s.lastIndex === match.index)
|
||||
s.lastIndex++;
|
||||
}
|
||||
|
||||
if (lastLastIndex === str.length) {
|
||||
if (!nativ.test.call(s, "") || lastLength)
|
||||
output.push("");
|
||||
} else {
|
||||
output.push(str.slice(lastLastIndex));
|
||||
}
|
||||
|
||||
return output.length > limit ? output.slice(0, limit) : output;
|
||||
};
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Private helper functions
|
||||
//---------------------------------
|
||||
|
||||
// Supporting function for `XRegExp`, `XRegExp.copyAsGlobal`, etc. Returns a copy of a `RegExp`
|
||||
// instance with a fresh `lastIndex` (set to zero), preserving properties required for named
|
||||
// capture. Also allows adding new flags in the process of copying the regex
|
||||
function clone (regex, additionalFlags) {
|
||||
if (!XRegExp.isRegExp(regex))
|
||||
throw TypeError("type RegExp expected");
|
||||
var x = regex._xregexp;
|
||||
regex = XRegExp(regex.source, getNativeFlags(regex) + (additionalFlags || ""));
|
||||
if (x) {
|
||||
regex._xregexp = {
|
||||
source: x.source,
|
||||
captureNames: x.captureNames ? x.captureNames.slice(0) : null
|
||||
};
|
||||
}
|
||||
return regex;
|
||||
}
|
||||
|
||||
function getNativeFlags (regex) {
|
||||
return (regex.global ? "g" : "") +
|
||||
(regex.ignoreCase ? "i" : "") +
|
||||
(regex.multiline ? "m" : "") +
|
||||
(regex.extended ? "x" : "") + // Proposed for ES4; included in AS3
|
||||
(regex.sticky ? "y" : "");
|
||||
}
|
||||
|
||||
function runTokens (pattern, index, scope, context) {
|
||||
var i = tokens.length,
|
||||
result, match, t;
|
||||
// Protect against constructing XRegExps within token handler and trigger functions
|
||||
isInsideConstructor = true;
|
||||
// Must reset `isInsideConstructor`, even if a `trigger` or `handler` throws
|
||||
try {
|
||||
while (i--) { // Run in reverse order
|
||||
t = tokens[i];
|
||||
if ((scope & t.scope) && (!t.trigger || t.trigger.call(context))) {
|
||||
t.pattern.lastIndex = index;
|
||||
match = t.pattern.exec(pattern); // Running the altered `exec` here allows use of named backreferences, etc.
|
||||
if (match && match.index === index) {
|
||||
result = {
|
||||
output: t.handler.call(context, match, scope),
|
||||
match: match
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
isInsideConstructor = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function indexOf (array, item, from) {
|
||||
if (Array.prototype.indexOf) // Use the native array method if available
|
||||
return array.indexOf(item, from);
|
||||
for (var i = from || 0; i < array.length; i++) {
|
||||
if (array[i] === item)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Built-in tokens
|
||||
//---------------------------------
|
||||
|
||||
// Augment XRegExp's regular expression syntax and flags. Note that when adding tokens, the
|
||||
// third (`scope`) argument defaults to `XRegExp.OUTSIDE_CLASS`
|
||||
|
||||
// Comment pattern: (?# )
|
||||
XRegExp.addToken(
|
||||
/\(\?#[^)]*\)/,
|
||||
function (match) {
|
||||
// Keep tokens separated unless the following token is a quantifier
|
||||
return nativ.test.call(quantifier, match.input.slice(match.index + match[0].length)) ? "" : "(?:)";
|
||||
}
|
||||
);
|
||||
|
||||
// Capturing group (match the opening parenthesis only).
|
||||
// Required for support of named capturing groups
|
||||
XRegExp.addToken(
|
||||
/\((?!\?)/,
|
||||
function () {
|
||||
this.captureNames.push(null);
|
||||
return "(";
|
||||
}
|
||||
);
|
||||
|
||||
// Named capturing group (match the opening delimiter only): (?<name>
|
||||
XRegExp.addToken(
|
||||
/\(\?<([$\w]+)>/,
|
||||
function (match) {
|
||||
this.captureNames.push(match[1]);
|
||||
this.hasNamedCapture = true;
|
||||
return "(";
|
||||
}
|
||||
);
|
||||
|
||||
// Named backreference: \k<name>
|
||||
XRegExp.addToken(
|
||||
/\\k<([\w$]+)>/,
|
||||
function (match) {
|
||||
var index = indexOf(this.captureNames, match[1]);
|
||||
// Keep backreferences separate from subsequent literal numbers. Preserve back-
|
||||
// references to named groups that are undefined at this point as literal strings
|
||||
return index > -1 ?
|
||||
"\\" + (index + 1) + (isNaN(match.input.charAt(match.index + match[0].length)) ? "" : "(?:)") :
|
||||
match[0];
|
||||
}
|
||||
);
|
||||
|
||||
// Empty character class: [] or [^]
|
||||
XRegExp.addToken(
|
||||
/\[\^?]/,
|
||||
function (match) {
|
||||
// For cross-browser compatibility with ES3, convert [] to \b\B and [^] to [\s\S].
|
||||
// (?!) should work like \b\B, but is unreliable in Firefox
|
||||
return match[0] === "[]" ? "\\b\\B" : "[\\s\\S]";
|
||||
}
|
||||
);
|
||||
|
||||
// Mode modifier at the start of the pattern only, with any combination of flags imsx: (?imsx)
|
||||
// Does not support x(?i), (?-i), (?i-m), (?i: ), (?i)(?m), etc.
|
||||
XRegExp.addToken(
|
||||
/^\(\?([imsx]+)\)/,
|
||||
function (match) {
|
||||
this.setFlag(match[1]);
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
// Whitespace and comments, in free-spacing (aka extended) mode only
|
||||
XRegExp.addToken(
|
||||
/(?:\s+|#.*)+/,
|
||||
function (match) {
|
||||
// Keep tokens separated unless the following token is a quantifier
|
||||
return nativ.test.call(quantifier, match.input.slice(match.index + match[0].length)) ? "" : "(?:)";
|
||||
},
|
||||
XRegExp.OUTSIDE_CLASS,
|
||||
function () {return this.hasFlag("x");}
|
||||
);
|
||||
|
||||
// Dot, in dotall (aka singleline) mode only
|
||||
XRegExp.addToken(
|
||||
/\./,
|
||||
function () {return "[\\s\\S]";},
|
||||
XRegExp.OUTSIDE_CLASS,
|
||||
function () {return this.hasFlag("s");}
|
||||
);
|
||||
|
||||
|
||||
//---------------------------------
|
||||
// Backward compatibility
|
||||
//---------------------------------
|
||||
|
||||
// Uncomment the following block for compatibility with XRegExp 1.0-1.2:
|
||||
/*
|
||||
XRegExp.matchWithinChain = XRegExp.matchChain;
|
||||
RegExp.prototype.addFlags = function (s) {return clone(this, s);};
|
||||
RegExp.prototype.execAll = function (s) {var r = []; XRegExp.iterate(s, this, function (m) {r.push(m);}); return r;};
|
||||
RegExp.prototype.forEachExec = function (s, f, c) {return XRegExp.iterate(s, this, f, c);};
|
||||
RegExp.prototype.validate = function (s) {var r = RegExp("^(?:" + this.source + ")$(?!\\s)", getNativeFlags(this)); if (this.global) this.lastIndex = 0; return s.search(r) === 0;};
|
||||
*/
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import os
|
||||
from mimetypes import guess_type
|
||||
from contextlib import closing
|
||||
from base64 import b64encode
|
||||
|
||||
from pecan.compat import quote
|
||||
|
||||
|
||||
def load_resource(filename):
|
||||
with closing(open(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
filename
|
||||
),
|
||||
'rb'
|
||||
)) as f:
|
||||
data = f.read()
|
||||
return 'data:%s;base64,%s' % (
|
||||
guess_type(filename)[0],
|
||||
quote(b64encode(data))
|
||||
)
|
||||
|
||||
pecan_image = load_resource('pecan.png')
|
||||
xregexp_js = load_resource('XRegExp.js')
|
||||
syntax_js = load_resource('shCore.js')
|
||||
syntax_css = load_resource('syntax.css')
|
||||
theme = load_resource('theme.css')
|
||||
brush = load_resource('brush-python.js')
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* SyntaxHighlighter
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter
|
||||
*
|
||||
* SyntaxHighlighter is donationware. If you are using it, please donate.
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
|
||||
*
|
||||
* @version
|
||||
* 3.0.83 (July 02 2010)
|
||||
*
|
||||
* @copyright
|
||||
* Copyright (C) 2004-2010 Alex Gorbatchev.
|
||||
*
|
||||
* @license
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
(function()
|
||||
{
|
||||
// CommonJS
|
||||
typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
|
||||
|
||||
function Brush()
|
||||
{
|
||||
// Contributed by Gheorghe Milas and Ahmad Sherif
|
||||
|
||||
var keywords = 'and assert break class continue def del elif else ' +
|
||||
'except exec finally for from global if import in is ' +
|
||||
'lambda not or pass print raise return try yield while';
|
||||
|
||||
var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' +
|
||||
'chr classmethod cmp coerce compile complex delattr dict dir ' +
|
||||
'divmod enumerate eval execfile file filter float format frozenset ' +
|
||||
'getattr globals hasattr hash help hex id input int intern ' +
|
||||
'isinstance issubclass iter len list locals long map max min next ' +
|
||||
'object oct open ord pow print property range raw_input reduce ' +
|
||||
'reload repr reversed round set setattr slice sorted staticmethod ' +
|
||||
'str sum super tuple type type unichr unicode vars xrange zip';
|
||||
|
||||
var special = 'None True False self cls class_';
|
||||
|
||||
this.regexList = [
|
||||
{ regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' },
|
||||
{ regex: /^\s*@\w+/gm, css: 'decorator' },
|
||||
{ regex: /(['\"]{3})([^\1])*?\1/gm, css: 'comments' },
|
||||
{ regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, css: 'string' },
|
||||
{ regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, css: 'string' },
|
||||
{ regex: /\+|\-|\*|\/|\%|=|==/gm, css: 'keyword' },
|
||||
{ regex: /\b\d+\.?\w*/g, css: 'value' },
|
||||
{ regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' },
|
||||
{ regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' },
|
||||
{ regex: new RegExp(this.getKeywords(special), 'gm'), css: 'color1' }
|
||||
];
|
||||
|
||||
this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
|
||||
};
|
||||
|
||||
Brush.prototype = new SyntaxHighlighter.Highlighter();
|
||||
Brush.aliases = ['py', 'python'];
|
||||
|
||||
SyntaxHighlighter.brushes.Python = Brush;
|
||||
|
||||
// CommonJS
|
||||
typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
|
||||
})();
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
1731
pecan/middleware/resources/shCore.js
vendored
1731
pecan/middleware/resources/shCore.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* SyntaxHighlighter
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter
|
||||
*
|
||||
* SyntaxHighlighter is donationware. If you are using it, please donate.
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
|
||||
*
|
||||
* @version
|
||||
* 3.0.83 (July 02 2010)
|
||||
*
|
||||
* @copyright
|
||||
* Copyright (C) 2004-2010 Alex Gorbatchev.
|
||||
*
|
||||
* @license
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
.syntaxhighlighter a,
|
||||
.syntaxhighlighter div,
|
||||
.syntaxhighlighter code,
|
||||
.syntaxhighlighter table,
|
||||
.syntaxhighlighter table td,
|
||||
.syntaxhighlighter table tr,
|
||||
.syntaxhighlighter table tbody,
|
||||
.syntaxhighlighter table thead,
|
||||
.syntaxhighlighter table caption,
|
||||
.syntaxhighlighter textarea {
|
||||
-moz-border-radius: 0 0 0 0 !important;
|
||||
-webkit-border-radius: 0 0 0 0 !important;
|
||||
background: none !important;
|
||||
border: 0 !important;
|
||||
bottom: auto !important;
|
||||
float: none !important;
|
||||
height: auto !important;
|
||||
left: auto !important;
|
||||
line-height: 1.1em !important;
|
||||
margin: 0 !important;
|
||||
outline: 0 !important;
|
||||
overflow: visible !important;
|
||||
padding: 0 !important;
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
text-align: left !important;
|
||||
top: auto !important;
|
||||
vertical-align: baseline !important;
|
||||
width: auto !important;
|
||||
box-sizing: content-box !important;
|
||||
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
|
||||
font-weight: normal !important;
|
||||
font-style: normal !important;
|
||||
font-size: 1em !important;
|
||||
min-height: inherit !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
.syntaxhighlighter {
|
||||
width: 100% !important;
|
||||
margin: 1em 0 1em 0 !important;
|
||||
position: relative !important;
|
||||
overflow: auto !important;
|
||||
font-size: 1em !important;
|
||||
}
|
||||
.syntaxhighlighter.source {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.syntaxhighlighter .bold {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.syntaxhighlighter .italic {
|
||||
font-style: italic !important;
|
||||
}
|
||||
.syntaxhighlighter .line {
|
||||
white-space: pre !important;
|
||||
}
|
||||
.syntaxhighlighter table {
|
||||
width: 100% !important;
|
||||
}
|
||||
.syntaxhighlighter table caption {
|
||||
text-align: left !important;
|
||||
padding: .5em 0 0.5em 1em !important;
|
||||
}
|
||||
.syntaxhighlighter table td.code {
|
||||
width: 100% !important;
|
||||
}
|
||||
.syntaxhighlighter table td.code .container {
|
||||
position: relative !important;
|
||||
}
|
||||
.syntaxhighlighter table td.code .container textarea {
|
||||
box-sizing: border-box !important;
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border: none !important;
|
||||
background: white !important;
|
||||
padding-left: 1em !important;
|
||||
overflow: hidden !important;
|
||||
white-space: pre !important;
|
||||
}
|
||||
.syntaxhighlighter table td.gutter .line {
|
||||
text-align: right !important;
|
||||
padding: 0 0.5em 0 1em !important;
|
||||
}
|
||||
.syntaxhighlighter table td.code .line {
|
||||
padding: 0 1em !important;
|
||||
}
|
||||
.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
|
||||
padding-left: 0em !important;
|
||||
}
|
||||
.syntaxhighlighter.show {
|
||||
display: block !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed table {
|
||||
display: none !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar {
|
||||
padding: 0.1em 0.8em 0em 0.8em !important;
|
||||
font-size: 1em !important;
|
||||
position: static !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar span {
|
||||
display: inline !important;
|
||||
margin-right: 1em !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar span a {
|
||||
padding: 0 !important;
|
||||
display: none !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar span a.expandSource {
|
||||
display: inline !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar {
|
||||
position: absolute !important;
|
||||
right: 1px !important;
|
||||
top: 1px !important;
|
||||
width: 11px !important;
|
||||
height: 11px !important;
|
||||
font-size: 10px !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar span.title {
|
||||
display: inline !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar a {
|
||||
display: block !important;
|
||||
text-align: center !important;
|
||||
text-decoration: none !important;
|
||||
padding-top: 1px !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar a.expandSource {
|
||||
display: none !important;
|
||||
}
|
||||
.syntaxhighlighter.ie {
|
||||
font-size: .9em !important;
|
||||
padding: 1px 0 1px 0 !important;
|
||||
}
|
||||
.syntaxhighlighter.ie .toolbar {
|
||||
line-height: 8px !important;
|
||||
}
|
||||
.syntaxhighlighter.ie .toolbar a {
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .line.alt1 .content,
|
||||
.syntaxhighlighter.printing .line.alt2 .content,
|
||||
.syntaxhighlighter.printing .line.highlighted .number,
|
||||
.syntaxhighlighter.printing .line.highlighted.alt1 .content,
|
||||
.syntaxhighlighter.printing .line.highlighted.alt2 .content {
|
||||
background: none !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .line .number {
|
||||
color: #bbbbbb !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .line .content {
|
||||
color: black !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
.syntaxhighlighter.printing a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
|
||||
color: black !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
|
||||
color: #008200 !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
|
||||
color: blue !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .keyword {
|
||||
color: #006699 !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .preprocessor {
|
||||
color: gray !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .variable {
|
||||
color: #aa7700 !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .value {
|
||||
color: #009900 !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .functions {
|
||||
color: #ff1493 !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .constants {
|
||||
color: #0066cc !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .script {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
|
||||
color: gray !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
|
||||
color: #ff1493 !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
|
||||
color: red !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
|
||||
color: black !important;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* SyntaxHighlighter
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter
|
||||
*
|
||||
* SyntaxHighlighter is donationware. If you are using it, please donate.
|
||||
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
|
||||
*
|
||||
* @version
|
||||
* 3.0.83 (July 02 2010)
|
||||
*
|
||||
* @copyright
|
||||
* Copyright (C) 2004-2010 Alex Gorbatchev.
|
||||
*
|
||||
* @license
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
.syntaxhighlighter {
|
||||
background-color: white !important;
|
||||
}
|
||||
.syntaxhighlighter .line.alt1 {
|
||||
background-color: white !important;
|
||||
}
|
||||
.syntaxhighlighter .line.alt2 {
|
||||
background-color: white !important;
|
||||
}
|
||||
.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
|
||||
background-color: #e0e0e0 !important;
|
||||
}
|
||||
.syntaxhighlighter .line.highlighted.number {
|
||||
color: black !important;
|
||||
}
|
||||
.syntaxhighlighter table caption {
|
||||
color: black !important;
|
||||
}
|
||||
.syntaxhighlighter .gutter {
|
||||
color: #afafaf !important;
|
||||
}
|
||||
.syntaxhighlighter .gutter .line {
|
||||
border-right: 3px solid #6ce26c !important;
|
||||
}
|
||||
.syntaxhighlighter .gutter .line.highlighted {
|
||||
background-color: #6ce26c !important;
|
||||
color: white !important;
|
||||
}
|
||||
.syntaxhighlighter.printing .line .content {
|
||||
border: none !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed {
|
||||
overflow: visible !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar {
|
||||
color: blue !important;
|
||||
background: white !important;
|
||||
border: 1px solid #6ce26c !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar a {
|
||||
color: blue !important;
|
||||
}
|
||||
.syntaxhighlighter.collapsed .toolbar a:hover {
|
||||
color: red !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar {
|
||||
color: white !important;
|
||||
background: #6ce26c !important;
|
||||
border: none !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar a {
|
||||
color: white !important;
|
||||
}
|
||||
.syntaxhighlighter .toolbar a:hover {
|
||||
color: black !important;
|
||||
}
|
||||
.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
|
||||
color: black !important;
|
||||
}
|
||||
.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
|
||||
color: #008200 !important;
|
||||
}
|
||||
.syntaxhighlighter .string, .syntaxhighlighter .string a {
|
||||
color: blue !important;
|
||||
}
|
||||
.syntaxhighlighter .keyword {
|
||||
color: #006699 !important;
|
||||
}
|
||||
.syntaxhighlighter .preprocessor {
|
||||
color: gray !important;
|
||||
}
|
||||
.syntaxhighlighter .variable {
|
||||
color: #aa7700 !important;
|
||||
}
|
||||
.syntaxhighlighter .value {
|
||||
color: #009900 !important;
|
||||
}
|
||||
.syntaxhighlighter .functions {
|
||||
color: #ff1493 !important;
|
||||
}
|
||||
.syntaxhighlighter .constants {
|
||||
color: #0066cc !important;
|
||||
}
|
||||
.syntaxhighlighter .script {
|
||||
font-weight: bold !important;
|
||||
color: #006699 !important;
|
||||
background-color: none !important;
|
||||
}
|
||||
.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
|
||||
color: gray !important;
|
||||
}
|
||||
.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
|
||||
color: #ff1493 !important;
|
||||
}
|
||||
.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.syntaxhighlighter .keyword {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
from wsgiref.util import setup_testing_defaults
|
||||
|
||||
from webtest import TestApp
|
||||
from six import b as b_
|
||||
|
||||
from pecan.middleware.debug import DebugMiddleware
|
||||
from pecan.tests import PecanTestCase
|
||||
|
||||
|
||||
class StripPasteVar(object):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
environ.pop('paste.testing')
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
class TestDebugMiddleware(PecanTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDebugMiddleware, self).setUp()
|
||||
|
||||
def conditional_error_app(environ, start_response):
|
||||
setup_testing_defaults(environ)
|
||||
if environ['PATH_INFO'] == '/error':
|
||||
assert 1 == 2
|
||||
start_response("200 OK", [('Content-type', 'text/plain')])
|
||||
return [b_('requested page returned')]
|
||||
self.app = TestApp(StripPasteVar(DebugMiddleware(
|
||||
conditional_error_app
|
||||
)))
|
||||
|
||||
def test_middleware_passes_through_when_no_exception_raised(self):
|
||||
r = self.app.get('/')
|
||||
assert r.status_int == 200
|
||||
assert r.body == b_('requested page returned')
|
||||
|
||||
def test_middleware_gives_stack_trace_on_errors(self):
|
||||
r = self.app.get('/error', expect_errors=True)
|
||||
assert r.status_int == 400
|
||||
assert b_('AssertionError') in r.body
|
||||
|
||||
def test_middleware_complains_in_multi_process_environment(self):
|
||||
|
||||
class MultiProcessApp(object):
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
environ['wsgi.multiprocess'] = True
|
||||
return self.app(environ, start_response)
|
||||
|
||||
def conditional_error_app(environ, start_response):
|
||||
start_response("200 OK", [('Content-type', 'text/plain')])
|
||||
return ['Hello, World!']
|
||||
|
||||
app = TestApp(MultiProcessApp(DebugMiddleware(conditional_error_app)))
|
||||
self.assertRaises(
|
||||
RuntimeError,
|
||||
app.get,
|
||||
'/'
|
||||
)
|
||||
|
||||
def test_middlware_allows_for_post_mortem_debugging(self):
|
||||
def patch_debugger(d):
|
||||
def _patched_debug_request():
|
||||
d.append(True)
|
||||
return _patched_debug_request
|
||||
|
||||
debugger = []
|
||||
|
||||
app = TestApp(StripPasteVar(DebugMiddleware(
|
||||
self.app,
|
||||
patch_debugger(debugger)
|
||||
)))
|
||||
|
||||
r = app.get('/error', expect_errors=True)
|
||||
assert r.status_int == 400
|
||||
|
||||
r = app.get('/__pecan_initiate_pdb__', expect_errors=True)
|
||||
assert len(debugger) > 0
|
||||
@@ -1683,41 +1683,6 @@ class TestNonCanonical(PecanTestCase):
|
||||
assert len(wrapped_apps) == 1
|
||||
|
||||
|
||||
class TestDebugging(PecanTestCase):
|
||||
def test_debugger_setup(self):
|
||||
class RootController(object):
|
||||
pass
|
||||
|
||||
def debugger():
|
||||
pass
|
||||
|
||||
app_conf = dict(
|
||||
debug=True,
|
||||
debugger=debugger
|
||||
)
|
||||
with mock.patch('pecan.middleware.debug.DebugMiddleware') \
|
||||
as patched_debug_middleware:
|
||||
app = make_app(RootController(), **app_conf)
|
||||
args, kwargs = patched_debug_middleware.call_args
|
||||
assert kwargs.get('debugger') == debugger
|
||||
|
||||
def test_invalid_debugger_setup(self):
|
||||
class RootController(object):
|
||||
pass
|
||||
|
||||
debugger = 'not_a_valid_entry_point'
|
||||
|
||||
app_conf = dict(
|
||||
debug=True,
|
||||
debugger=debugger
|
||||
)
|
||||
with mock.patch('pecan.middleware.debug.DebugMiddleware') \
|
||||
as patched_debug_middleware:
|
||||
app = make_app(RootController(), **app_conf)
|
||||
args, kwargs = patched_debug_middleware.call_args
|
||||
assert kwargs.get('debugger') is None
|
||||
|
||||
|
||||
class TestLogging(PecanTestCase):
|
||||
|
||||
def test_logging_setup(self):
|
||||
|
||||
Reference in New Issue
Block a user