Clearing out old site code
This commit is contained in:
-17
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Page Not Found
|
||||
---
|
||||
{{#extend "layout"}}
|
||||
{{#content "body"}}
|
||||
<div class="error copy">
|
||||
<h1>404: Not Found</h1>
|
||||
|
||||
<blockquote>
|
||||
Some people, when confronted with a problem, think<br/>
|
||||
“I know, I'll use regular expressions.” Now they have two problems.
|
||||
</blockquote>
|
||||
|
||||
<p>Apparently, you have three problems…because the page you requested cannot be found.</p>
|
||||
</div>
|
||||
{{/content}}
|
||||
{{/extend}}
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Changelog
|
||||
---
|
||||
{{#extend "layout"}}
|
||||
{{#content "body"}}
|
||||
<div class="copy changelog">
|
||||
<dl>
|
||||
{{#each changelog}}
|
||||
<dt>{{label}}</dt>
|
||||
{{#each changes}}
|
||||
<dd>{{{this}}}</dd>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</dl>
|
||||
</div>
|
||||
{{/content}}
|
||||
{{/extend}}
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
title: Documentation
|
||||
---
|
||||
{{#extend "layout"}}
|
||||
{{#content "body"}}
|
||||
<div class="copy documentation">
|
||||
<section>
|
||||
<h1>Reading Railroad Diagrams</h1>
|
||||
|
||||
<p>The images generated by Regexper are commonly referred to as "Railroad Diagrams". These diagram are a straight-forward way to illustrate what can sometimes become very complicated processing in a regular expression, with nested looping and optional elements. The easiest way to read these diagrams to to start at the left and follow the lines to the right. If you encounter a branch, then there is the option of following one of multiple paths (and those paths can loop back to earlier parts of the diagram). In order for a string to successfully match the regular expression in a diagram, you must be able to fulfill each part of the diagram as you move from left to right and proceed through the entire diagram to the end.</p>
|
||||
|
||||
<figure class="shift-right" data-expr="Lions(?: and|,) tigers,? and bears\. Oh my!"></figure>
|
||||
|
||||
<p>As an example, this expression will match "Lions and tigers and bears. Oh my!" or the more grammatically correct "Lions, tigers, and bears. Oh my!" (with or without an Oxford comma). The diagram first matches the string "Lions"; you cannot proceed without that in your input. Then there is a choice between a comma or the string " and". No matter what choice you make, the input string must then contain " tigers" followed by an optional comma (your path can either go through the comma or around it). Finally the string must end with " and bears. Oh my!".</p>
|
||||
|
||||
<section>
|
||||
<h2>Basic parts of these diagrams</h2>
|
||||
|
||||
<p>The simplest pieces of these diagrams to understand are the parts that match some specific bit of text without an options. They are: Literals, Escape sequences, and "Any charater".</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>Literals</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="A literal example"></figure>
|
||||
|
||||
<p>Literals match an exact string of text. They're displayed in a light blue box, and the contents are quoted (to make it easier to see any leading or trailing whitespace).</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Escape sequences</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="\w\x7f\u00bb\1\0"></figure>
|
||||
|
||||
<p>Escape sequences are displayed in a green box and contain a description of the type of character(s) they will match.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>"Any character"</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="."></figure>
|
||||
|
||||
<p>"Any character" is similar to an escape sequence. It matches any single character.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Character Sets</h2>
|
||||
|
||||
<figure class="shift-left" data-expr="[#a-z\n][^$0-9\b]"></figure>
|
||||
|
||||
<p>Character sets will match or not match a collection of individual characters. They are shown as a box containing literals and escape sequences. The label at the top indicates that the character set will match "One of" the contained items or "None of" the contained items.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Subexpressions</h2>
|
||||
|
||||
<figure class="shift-left" data-expr="(example\s)(?=content)"></figure>
|
||||
|
||||
<p>Subexpressions are indicated by a dotted outline around the items that are in the expression. Captured subexpressions are labeled with the group number they will be captured under. Positive and negative lookahead are labeled as such.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Alternation</h2>
|
||||
|
||||
<figure class="shift-left" data-expr="one\s|two\W|three\t|four\n"></figure>
|
||||
|
||||
<p>Alternation provides choices for the regular experssion. It is indicated by the path for the expression fanning out into a number of choices.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Quantifiers</h2>
|
||||
|
||||
<p>Quantifiers indicate if part of the expression should be repeated or optional. They are displayed similarly to Alternation, by the path through the diagram branching (and possibly looping back on itself). Unless indicated by an arrow on the path, the preferred path is to continue going straight.</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>Zero-or-more</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:greedy)*">
|
||||
<figcaption>Greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:non-greedy)*?">
|
||||
<figcaption>Non-greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>The zero-or-more quantifier matches any number of repetitions of the pattern.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Required</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:greedy)+">
|
||||
<figcaption>Greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:non-greedy)+?">
|
||||
<figcaption>Non-greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>The required quantifier matches one or more repetitions of the pattern. Note that it does not have the path that allows the pattern to be skipped like the zero-or-more quantifier.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Optional</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:greedy)?">
|
||||
<figcaption>Greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:non-greedy)??">
|
||||
<figcaption>Non-greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>The optional quantifier matches the pattern at most once. Note that it does not have the path that allows the pattern to loop back on itself like the zero-or-more or required quantifiers.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Range</h3>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:greedy){5,10}">
|
||||
<figcaption>Greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="shift-left" data-expr="(?:non-greedy){5,10}?">
|
||||
<figcaption>Non-greedy quantifier</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>The ranged quantifier specifies a number of times the pattern may be repeated. The two examples provided here both have a range of "{5,10}", the label for the looping branch indicates the number of times that branch may be followed. The values are one less than specified in the expression since the pattern would have to be matched once before repeating it is an option. So, for these examples, the pattern would be matched once and then the loop would be followed 4 to 9 times, for a total of 5 to 10 matches of the pattern.</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
{{/content}}
|
||||
|
||||
{{#content "footer" mode="append"}}
|
||||
<script src="/js/main.js" async defer></script>
|
||||
{{/content}}
|
||||
{{/extend}}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB |
@@ -1,16 +0,0 @@
|
||||
# humanstxt.org/
|
||||
# The humans responsible & technology colophon
|
||||
|
||||
# TEAM
|
||||
|
||||
Creator: Jeff Avallone
|
||||
Site: http://github.com/javallone
|
||||
Twitter: @javallone
|
||||
|
||||
# THANKS
|
||||
|
||||
strfriend.com for the idea, whatever happened to you?
|
||||
|
||||
# TECHNOLOGY COLOPHON
|
||||
|
||||
HTML5, CSS3, SVG, Sass, Open Iconic
|
||||
@@ -1,35 +0,0 @@
|
||||
{{#extend "layout"}}
|
||||
{{#content "body"}}
|
||||
<div class="application">
|
||||
<form id="regexp-form">
|
||||
<textarea id="regexp-input" autofocus="autofocus" placeholder="Enter JavaScript-style regular expression to display"></textarea>
|
||||
<button type="submit">Display</button>
|
||||
|
||||
<ul class="inline-list">
|
||||
<li class="download-svg">
|
||||
<a href="#" class="inline-icon" data-action="download-svg" download="image.svg" type="image/svg+xml">{{icon "#data-transfer-download"}}Download SVG</a>
|
||||
</li>
|
||||
<li class="download-png">
|
||||
<a href="#" class="inline-icon" data-action="download-png" download="image.png" type="image/png">{{icon "#data-transfer-download"}}Download PNG</a>
|
||||
</li>
|
||||
<li class="permalink">
|
||||
<a href="#" class="inline-icon" data-action="permalink">{{icon "#link-intact"}}Permalink</a>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<div id="error"></div>
|
||||
|
||||
<ul id="warnings"></ul>
|
||||
|
||||
<div id="regexp-render"></div>
|
||||
</div>
|
||||
|
||||
{{/content}}
|
||||
|
||||
{{#content "footer" mode="append"}}
|
||||
<script src="/js/main.js" async defer></script>
|
||||
{{/content}}
|
||||
{{/extend}}
|
||||
@@ -1,38 +0,0 @@
|
||||
// This file contains code to start up pages on the site, and other code that
|
||||
// is not directly related to parsing and display of regular expressions.
|
||||
//
|
||||
// Since the code in this is executed immediately, it is all but impossible to
|
||||
// test. Therefore, this code is kept as simple as possible to reduce the need
|
||||
// to run it through automated tests.
|
||||
|
||||
import util from './util.js';
|
||||
import Regexper from './regexper.js';
|
||||
import Parser from './parser/javascript.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
(function() {
|
||||
// Initialize the main page of the site. Functionality is kept in the
|
||||
// [Regexper class](./regexper.html).
|
||||
if (document.body.querySelector('#content .application')) {
|
||||
let regexper = new Regexper(document.body);
|
||||
|
||||
regexper.detectBuggyHash();
|
||||
regexper.bindListeners();
|
||||
|
||||
util.tick().then(() => {
|
||||
window.dispatchEvent(util.customEvent('hashchange'));
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize other pages on the site (specifically the documentation page).
|
||||
// Any element with a `data-expr` attribute will contain a rendering of the
|
||||
// provided regular expression.
|
||||
_.each(document.querySelectorAll('[data-expr]'), element => {
|
||||
new Parser(element, { keepContent: true })
|
||||
.parse(element.getAttribute('data-expr'))
|
||||
.then(parser => {
|
||||
parser.render();
|
||||
})
|
||||
.catch(util.exposeError);
|
||||
});
|
||||
}());
|
||||
@@ -1,112 +0,0 @@
|
||||
// Entry point for the JavaScript-flavor regular expression parsing and
|
||||
// rendering. Actual parsing code is in
|
||||
// [parser.js](./javascript/parser.html) and the grammar file. Rendering code
|
||||
// is contained in the various subclasses of
|
||||
// [Node](./javascript/node.html)
|
||||
|
||||
import Snap from 'snapsvg';
|
||||
import _ from 'lodash';
|
||||
|
||||
import util from '../util.js';
|
||||
import javascript from './javascript/parser.js';
|
||||
import ParserState from './javascript/parser_state.js';
|
||||
|
||||
export default class Parser {
|
||||
// - __container__ - DOM node that will contain the rendered expression
|
||||
// - __options.keepContent__ - Boolean indicating if content of the container
|
||||
// should be preserved after rendering. Defaults to false (don't keep
|
||||
// contents)
|
||||
constructor(container, options) {
|
||||
this.options = options || {};
|
||||
_.defaults(this.options, {
|
||||
keepContent: false
|
||||
});
|
||||
|
||||
this.container = container;
|
||||
|
||||
// The [ParserState](./javascript/parser_state.html) instance is used to
|
||||
// communicate between the parser and a running render, and to update the
|
||||
// progress bar for the running render.
|
||||
this.state = new ParserState(this.container.querySelector('.progress div'));
|
||||
}
|
||||
|
||||
// DOM node that will contain the rendered expression. Setting this will add
|
||||
// the base markup necessary for rendering the expression, and set the
|
||||
// `svg-container` class
|
||||
set container(cont) {
|
||||
this._container = cont;
|
||||
this._container.innerHTML = [
|
||||
document.querySelector('#svg-container-base').innerHTML,
|
||||
this.options.keepContent ? this.container.innerHTML : ''
|
||||
].join('');
|
||||
this._addClass('svg-container');
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
// Helper method to simplify adding classes to the container.
|
||||
_addClass(className) {
|
||||
this.container.className = _(this.container.className.split(' '))
|
||||
.union([className])
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Helper method to simplify removing classes from the container.
|
||||
_removeClass(className) {
|
||||
this.container.className = _(this.container.className.split(' '))
|
||||
.without(className)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Parse a regular expression into a tree of
|
||||
// [Nodes](./javascript/node.html) that can then be used to render an SVG.
|
||||
// - __expression__ - Regular expression to parse.
|
||||
parse(expression) {
|
||||
this._addClass('loading');
|
||||
|
||||
// Allow the browser to repaint before parsing so that the loading bar is
|
||||
// displayed before the (possibly lengthy) parsing begins.
|
||||
return util.tick().then(() => {
|
||||
javascript.Parser.SyntaxNode.state = this.state;
|
||||
|
||||
this.parsed = javascript.parse(expression.replace(/\n/g, '\\n'));
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
// Render the parsed expression to an SVG.
|
||||
render() {
|
||||
let svg = Snap(this.container.querySelector('svg'));
|
||||
|
||||
return this.parsed.render(svg.group())
|
||||
// Once rendering is complete, the rendered expression is positioned and
|
||||
// the SVG resized to create some padding around the image contents.
|
||||
.then(result => {
|
||||
let box = result.getBBox();
|
||||
|
||||
result.transform(Snap.matrix()
|
||||
.translate(10 - box.x, 10 - box.y));
|
||||
svg.attr({
|
||||
width: box.width + 20,
|
||||
height: box.height + 20
|
||||
});
|
||||
})
|
||||
// Stop and remove loading indicator after render is totally complete.
|
||||
.then(() => {
|
||||
this._removeClass('loading');
|
||||
this.container.removeChild(this.container.querySelector('.progress'));
|
||||
});
|
||||
}
|
||||
|
||||
// Cancels any currently in-progress render.
|
||||
cancel() {
|
||||
this.state.cancelRender = true;
|
||||
}
|
||||
|
||||
// Returns any warnings that may have been set during the rendering process.
|
||||
get warnings() {
|
||||
return this.state.warnings;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
_render() {
|
||||
return this.renderLabel(this.label).then(label => label.addClass('anchor'));
|
||||
},
|
||||
|
||||
setup() {
|
||||
if (this.textValue === '^') {
|
||||
this.label = 'Start of line';
|
||||
} else {
|
||||
this.label = 'End of line';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
// AnyCharacter nodes are for `*` regular expression syntax. They are rendered
|
||||
// as just an "any character" label.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'any-character',
|
||||
|
||||
_render() {
|
||||
return this.renderLabel('any character');
|
||||
}
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
// Charset nodes are used for `[abc1-9]` regular expression syntax. It is
|
||||
// rendered as a labeled box with each literal, escape, and range rendering
|
||||
// handled by the nested node(s).
|
||||
|
||||
import util from '../../util.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'charset',
|
||||
|
||||
definedProperties: {
|
||||
// Default anchor is overridden to move it down so that it connects at the
|
||||
// middle of the box that wraps all of the charset parts, instead of the
|
||||
// middle of the container, which would take the label into account.
|
||||
_anchor: {
|
||||
get: function() {
|
||||
var matrix = this.transform().localMatrix;
|
||||
|
||||
return {
|
||||
ay: matrix.y(0, this.partContainer.getBBox().cy)
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Renders the charset into the currently set container.
|
||||
_render() {
|
||||
this.partContainer = this.container.group();
|
||||
|
||||
// Renders each part of the charset into the part container.
|
||||
return Promise.all(_.map(this.elements,
|
||||
part => part.render(this.partContainer.group())
|
||||
))
|
||||
.then(() => {
|
||||
// Space the parts of the charset vertically in the part container.
|
||||
util.spaceVertically(this.elements, {
|
||||
padding: 5
|
||||
});
|
||||
|
||||
// Label the part container.
|
||||
return this.renderLabeledBox(this.label, this.partContainer, {
|
||||
padding: 5
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setup() {
|
||||
// The label for the charset will be:
|
||||
// - "One of:" for charsets of the form: `[abc]`.
|
||||
// - "None of:" for charsets of the form: `[^abc]`.
|
||||
this.label = (this.properties.invert.textValue === '^') ? 'None of:' : 'One of:';
|
||||
|
||||
// Removes any duplicate parts from the charset. This is based on the type
|
||||
// and text value of the part, so `[aa]` will have only one item, but
|
||||
// `[a\x61]` will contain two since the first matches "a" and the second
|
||||
// matches 0x61 (even though both are an "a").
|
||||
this.elements = _.uniqBy(this.properties.parts.elements,
|
||||
part => `${part.type}:${part.textValue}`);
|
||||
|
||||
// Include a warning for charsets that attempt to match `\c` followed by
|
||||
// any character other than A-Z (case insensitive). Charsets like `[\c@]`
|
||||
// behave differently in different browsers. Some match the character
|
||||
// reference by the control charater escape, others match "\", "c", or "@",
|
||||
// and some do not appear to match anything.
|
||||
if (this.textValue.match(/\\c[^a-zA-Z]/)) {
|
||||
this.state.warnings.push(`The character set "${this.textValue}" contains the \\c escape followed by a character other than A-Z. This can lead to different behavior depending on browser. The representation here is the most common interpretation.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// CharsetEscape nodes are for escape sequences inside of character sets. They
|
||||
// differ from other [Escape](./escape.html) nodes in that `\b` matches a
|
||||
// backspace character instead of a word boundary.
|
||||
|
||||
import _ from 'lodash';
|
||||
import Escape from './escape.js';
|
||||
|
||||
export default _.extend({}, Escape, {
|
||||
type: 'charset-escape',
|
||||
|
||||
b: ['backspace', 0x08, true]
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
// CharsetRange nodes are used for `[a-z]` regular expression syntax. The two
|
||||
// literal or escape nodes are rendered with a hyphen between them.
|
||||
|
||||
import util from '../../util.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'charset-range',
|
||||
|
||||
// Renders the charset range into the currently set container
|
||||
_render() {
|
||||
let contents = [
|
||||
this.first,
|
||||
this.container.text(0, 0, '-'),
|
||||
this.last
|
||||
];
|
||||
|
||||
// Render the nodes of the range.
|
||||
return Promise.all([
|
||||
this.first.render(this.container.group()),
|
||||
this.last.render(this.container.group())
|
||||
])
|
||||
.then(() => {
|
||||
// Space the nodes and hyphen horizontally.
|
||||
util.spaceHorizontally(contents, {
|
||||
padding: 5
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setup() {
|
||||
// The two nodes for the range. In `[a-z]` these would be
|
||||
// [Literal](./literal.html) nodes for "a" and "z".
|
||||
this.first = this.properties.first;
|
||||
this.last = this.properties.last;
|
||||
|
||||
// Report invalid expression when extents of the range are out of order.
|
||||
if (this.first.ordinal > this.last.ordinal) {
|
||||
throw `Range out of order in character class: ${this.textValue}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
// Escape nodes are used for escape sequences. It is rendered as a label with
|
||||
// the description of the escape and the numeric code it matches when
|
||||
// appropriate.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
function hex(value) {
|
||||
var str = value.toString(16).toUpperCase();
|
||||
|
||||
if (str.length < 2) {
|
||||
str = '0' + str;
|
||||
}
|
||||
|
||||
return `(0x${str})`;
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'escape',
|
||||
|
||||
// Renders the escape into the currently set container.
|
||||
_render() {
|
||||
return this.renderLabel(this.label)
|
||||
.then(label => {
|
||||
label.select('rect').attr({
|
||||
rx: 3,
|
||||
ry: 3
|
||||
});
|
||||
return label;
|
||||
});
|
||||
},
|
||||
|
||||
setup() {
|
||||
let addHex;
|
||||
|
||||
// The escape code. For an escape such as `\b` it would be "b".
|
||||
this.code = this.properties.esc.properties.code.textValue;
|
||||
// The argument. For an escape such as `\xab` it would be "ab".
|
||||
this.arg = this.properties.esc.properties.arg.textValue;
|
||||
// Retrieves the label, ordinal value, an flag to control adding hex value
|
||||
// from the escape code mappings
|
||||
[this.label, this.ordinal, addHex] = _.result(this, this.code);
|
||||
|
||||
// When requested, add hex code to the label.
|
||||
if (addHex) {
|
||||
this.label = `${this.label} ${hex(this.ordinal)}`;
|
||||
}
|
||||
},
|
||||
|
||||
// Escape code mappings
|
||||
b: ['word boundary', -1, false],
|
||||
B: ['non-word boundary', -1, false],
|
||||
d: ['digit', -1, false],
|
||||
D: ['non-digit', -1, false],
|
||||
f: ['form feed', 0x0c, true],
|
||||
n: ['line feed', 0x0a, true],
|
||||
r: ['carriage return', 0x0d, true],
|
||||
s: ['white space', -1, false],
|
||||
S: ['non-white space', -1, false],
|
||||
t: ['tab', 0x09, true],
|
||||
v: ['vertical tab', 0x0b, true],
|
||||
w: ['word', -1, false],
|
||||
W: ['non-word', -1, false],
|
||||
1: ['Back reference (group = 1)', -1, false],
|
||||
2: ['Back reference (group = 2)', -1, false],
|
||||
3: ['Back reference (group = 3)', -1, false],
|
||||
4: ['Back reference (group = 4)', -1, false],
|
||||
5: ['Back reference (group = 5)', -1, false],
|
||||
6: ['Back reference (group = 6)', -1, false],
|
||||
7: ['Back reference (group = 7)', -1, false],
|
||||
8: ['Back reference (group = 8)', -1, false],
|
||||
9: ['Back reference (group = 9)', -1, false],
|
||||
0: function() {
|
||||
if (this.arg) {
|
||||
return [`octal: ${this.arg}`, parseInt(this.arg, 8), true];
|
||||
} else {
|
||||
return ['null', 0, true];
|
||||
}
|
||||
},
|
||||
c() {
|
||||
return [`ctrl-${this.arg.toUpperCase()}`, this.arg.toUpperCase().charCodeAt(0) - 64, true];
|
||||
},
|
||||
x() {
|
||||
return [`0x${this.arg.toUpperCase()}`, parseInt(this.arg, 16), false];
|
||||
},
|
||||
u() {
|
||||
return [`U+${this.arg.toUpperCase()}`, parseInt(this.arg, 16), false];
|
||||
}
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
grammar JavascriptRegexp
|
||||
root <- ( ( "/" regexp "/" flags:[yigmu]* ) / regexp flags:""? ) <Root>
|
||||
regexp <- match:match alternates:( "|" match )* <Regexp>
|
||||
match <- (!repeat) parts:match_fragment* <Match>
|
||||
anchor <- ( "^" / "$" ) <Anchor>
|
||||
match_fragment <- content:( anchor / subexp / charset / terminal ) repeat:repeat? <MatchFragment>
|
||||
repeat <- spec:( repeat_any / repeat_required / repeat_optional / repeat_spec ) greedy:"?"? <Repeat>
|
||||
repeat_any <- "*" <RepeatAny>
|
||||
repeat_required <- "+" <RepeatRequired>
|
||||
repeat_optional <- "?" <RepeatOptional>
|
||||
repeat_spec <- ( "{" min:[0-9]+ "," max:[0-9]+ "}"
|
||||
/ "{" min:[0-9]+ ",}"
|
||||
/ "{" exact:[0-9]+ "}" ) <RepeatSpec>
|
||||
subexp <- "(" capture:( "?:" / "?=" / "?!" )? regexp ")" <Subexp>
|
||||
charset <- "[" invert:"^"? parts:( charset_range / charset_terminal )* "]" <Charset>
|
||||
charset_range <- first:charset_range_terminal "-" last:charset_range_terminal <CharsetRange>
|
||||
charset_terminal <- charset_escape <CharsetEscape>
|
||||
/ charset_literal <Literal>
|
||||
charset_range_terminal <- charset_range_escape <CharsetEscape>
|
||||
/ charset_literal <Literal>
|
||||
charset_escape <- "\\" esc:(
|
||||
code:[bdDfnrsStvwW] arg:""?
|
||||
/ control_escape
|
||||
/ octal_escape
|
||||
/ hex_escape
|
||||
/ unicode_escape
|
||||
/ null_escape )
|
||||
charset_range_escape <- "\\" esc:(
|
||||
code:[bfnrtv] arg:""?
|
||||
/ control_escape
|
||||
/ octal_escape
|
||||
/ hex_escape
|
||||
/ unicode_escape
|
||||
/ null_escape )
|
||||
charset_literal <- ( ""? literal:[^\\\]] )
|
||||
/ ( literal:"\\" &"c" )
|
||||
/ ( "\\" literal:[^bdDfnrsStvwW] )
|
||||
terminal <- "." <AnyCharacter>
|
||||
/ escape <Escape>
|
||||
/ literal <Literal>
|
||||
escape <- "\\" esc:(
|
||||
code:[bBdDfnrsStvwW1-9] arg:""?
|
||||
/ control_escape
|
||||
/ octal_escape
|
||||
/ hex_escape
|
||||
/ unicode_escape
|
||||
/ null_escape )
|
||||
literal <- ( ""? literal:[^|\\/.\[\(\)?+*$^] )
|
||||
/ ( literal:"\\" &"c" )
|
||||
/ ( "\\" literal:. )
|
||||
|
||||
control_escape <- code:"c" arg:[a-zA-Z]
|
||||
octal_escape <- code:"0" arg:[0-7]+
|
||||
hex_escape <- code:"x" arg:( [0-9a-fA-F] [0-9a-fA-F] )
|
||||
unicode_escape <- code:"u" arg:( [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] )
|
||||
null_escape <- code:"0" arg:""?
|
||||
@@ -1,43 +0,0 @@
|
||||
// Literal nodes are for plain strings in the regular expression. They are
|
||||
// rendered as labels with the value of the literal quoted.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'literal',
|
||||
|
||||
// Renders the literal into the currently set container.
|
||||
_render() {
|
||||
return this.renderLabel(['\u201c', this.literal, '\u201d'])
|
||||
.then(label => {
|
||||
let spans = label.selectAll('tspan');
|
||||
|
||||
// The quote marks get some styling to lighten their color so they are
|
||||
// distinct from the actual literal value.
|
||||
spans[0].addClass('quote');
|
||||
spans[2].addClass('quote');
|
||||
|
||||
label.select('rect').attr({
|
||||
rx: 3,
|
||||
ry: 3
|
||||
});
|
||||
|
||||
return label;
|
||||
});
|
||||
},
|
||||
|
||||
// Merges this literal with another. Literals come back as single characters
|
||||
// during parsing, and must be post-processed into multi-character literals
|
||||
// for rendering. This processing is done in [Match](./match.html).
|
||||
merge(other) {
|
||||
this.literal += other.literal;
|
||||
},
|
||||
|
||||
setup() {
|
||||
// Value of the literal.
|
||||
this.literal = this.properties.literal.textValue;
|
||||
// Ordinal value of the literal for use in
|
||||
// [CharsetRange](./charset_range.html).
|
||||
this.ordinal = this.literal.charCodeAt(0);
|
||||
}
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
// Match nodes are used for the parts of a regular expression between `|`
|
||||
// symbols. They consist of a series of [MatchFragment](./match_fragment.html)
|
||||
// nodes. Optional `^` and `$` symbols are also allowed at the beginning and
|
||||
// end of the Match.
|
||||
|
||||
import util from '../../util.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'match',
|
||||
|
||||
definedProperties: {
|
||||
// Default anchor is overridden to attach the left point of the anchor to
|
||||
// the first element, and the right point to the last element.
|
||||
_anchor: {
|
||||
get: function() {
|
||||
var start = util.normalizeBBox(this.start.getBBox()),
|
||||
end = util.normalizeBBox(this.end.getBBox()),
|
||||
matrix = this.transform().localMatrix;
|
||||
|
||||
return {
|
||||
ax: matrix.x(start.ax, start.ay),
|
||||
ax2: matrix.x(end.ax2, end.ay),
|
||||
ay: matrix.y(start.ax, start.ay)
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Renders the match into the currently set container.
|
||||
_render() {
|
||||
// Render each of the match fragments.
|
||||
let partPromises = _.map(this.parts, part => part.render(this.container.group())),
|
||||
items = _(partPromises).compact().value();
|
||||
|
||||
// Handle the situation where a regular expression of `()` is rendered.
|
||||
// This leads to a Match node with no fragments. Something must be rendered
|
||||
// so that the anchor can be calculated based on it.
|
||||
//
|
||||
// Furthermore, the content rendered must have height and width or else the
|
||||
// anchor calculations fail.
|
||||
if (items.length === 0) {
|
||||
items = [this.container.group().path('M0,0h10')];
|
||||
}
|
||||
|
||||
return Promise.all(items)
|
||||
.then(items => {
|
||||
// Find SVG elements to be used when calculating the anchor.
|
||||
this.start = _.first(items);
|
||||
this.end = _.last(items);
|
||||
|
||||
util.spaceHorizontally(items, {
|
||||
padding: 10
|
||||
});
|
||||
|
||||
// Add lines between each item.
|
||||
this.container.prepend(
|
||||
this.container.path(this.connectorPaths(items).join('')));
|
||||
});
|
||||
},
|
||||
|
||||
// Returns an array of SVG path strings between each item.
|
||||
// - __items__ - Array of SVG elements or nodes.
|
||||
connectorPaths(items) {
|
||||
let prev, next;
|
||||
|
||||
prev = util.normalizeBBox(_.first(items).getBBox());
|
||||
return _.map(items.slice(1), item => {
|
||||
try {
|
||||
next = util.normalizeBBox(item.getBBox());
|
||||
return `M${prev.ax2},${prev.ay}H${next.ax}`;
|
||||
}
|
||||
finally {
|
||||
prev = next;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setup() {
|
||||
// Merged list of MatchFragments to be rendered.
|
||||
this.parts = _.reduce(this.properties.parts.elements, function(result, node) {
|
||||
var last = _.last(result);
|
||||
|
||||
if (last && node.canMerge && last.canMerge) {
|
||||
// Merged the content of `node` into `last` when possible. This also
|
||||
// discards `node` in the process since `result` has not been changed.
|
||||
last.content.merge(node.content);
|
||||
} else {
|
||||
// `node` cannot be merged with the previous node, so it is added to
|
||||
// the list of parts.
|
||||
result.push(node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// When there is only one part, then proxy to the part.
|
||||
if (this.parts.length === 1) {
|
||||
this.proxy = this.parts[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
// MatchFragment nodes are part of a [Match](./match.html) followed by an
|
||||
// optional [Repeat](./repeat.html) node. If no repeat is applied, then
|
||||
// rendering is proxied to the content node.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'match-fragment',
|
||||
|
||||
definedProperties: {
|
||||
// Default anchor is overridden to apply an transforms from the fragment
|
||||
// to its content's anchor. Essentially, the fragment inherits the anchor
|
||||
// of its content.
|
||||
_anchor: {
|
||||
get: function() {
|
||||
var anchor = this.content.getBBox(),
|
||||
matrix = this.transform().localMatrix;
|
||||
|
||||
return {
|
||||
ax: matrix.x(anchor.ax, anchor.ay),
|
||||
ax2: matrix.x(anchor.ax2, anchor.ay),
|
||||
ay: matrix.y(anchor.ax, anchor.ay)
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Renders the fragment into the currently set container.
|
||||
_render() {
|
||||
return this.content.render(this.container.group())
|
||||
.then(() => {
|
||||
let box, paths;
|
||||
|
||||
// Contents must be transformed based on the repeat that is applied.
|
||||
this.content.transform(this.repeat.contentPosition);
|
||||
|
||||
box = this.content.getBBox();
|
||||
|
||||
// Add skip or repeat paths to the container.
|
||||
paths = _.flatten([
|
||||
this.repeat.skipPath(box),
|
||||
this.repeat.loopPath(box)
|
||||
]);
|
||||
|
||||
this.container.prepend(
|
||||
this.container.path(paths.join('')));
|
||||
|
||||
this.loopLabel();
|
||||
});
|
||||
},
|
||||
|
||||
// Renders label for the loop path indicating how many times the content may
|
||||
// be matched.
|
||||
loopLabel() {
|
||||
let labelStr = this.repeat.label,
|
||||
tooltipStr = this.repeat.tooltip;
|
||||
|
||||
if (labelStr) {
|
||||
let label = this.container.text(0, 0, [labelStr])
|
||||
.addClass('repeat-label'),
|
||||
labelBox = label.getBBox(),
|
||||
box = this.getBBox();
|
||||
|
||||
if (tooltipStr) {
|
||||
let tooltip = this.container.el('title')
|
||||
.append(this.container.text(0, 0, tooltipStr));
|
||||
label.append(tooltip);
|
||||
}
|
||||
|
||||
label.transform(Snap.matrix().translate(
|
||||
box.x2 - labelBox.width - (this.repeat.hasSkip ? 5 : 0),
|
||||
box.y2 + labelBox.height));
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
// Then content of the fragment.
|
||||
this.content = this.properties.content;
|
||||
// The repetition rule for the fragment.
|
||||
this.repeat = this.properties.repeat;
|
||||
|
||||
if (!this.repeat.hasLoop && !this.repeat.hasSkip) {
|
||||
// For fragments without a skip or loop, rendering is proxied to the
|
||||
// content. Also set flag indicating that contents can be merged if the
|
||||
// content is a literal node.
|
||||
this.canMerge = (this.content.type === 'literal');
|
||||
this.proxy = this.content;
|
||||
} else {
|
||||
// Fragments that have skip or loop lines cannot be merged with others.
|
||||
this.canMerge = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,186 +0,0 @@
|
||||
// Base class for all nodes in the parse tree. An instance of this class is
|
||||
// created for each parsed node, and then extended with one of the node-type
|
||||
// modules.
|
||||
import util from '../../util.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class Node {
|
||||
// Arguments passed in are defined by the canopy tool.
|
||||
constructor(textValue, offset, elements, properties) {
|
||||
this.textValue = textValue;
|
||||
this.offset = offset;
|
||||
this.elements = elements || [];
|
||||
|
||||
this.properties = properties;
|
||||
|
||||
// This is the current parser state (an instance
|
||||
// [ParserState](./parser_state.html).)
|
||||
this.state = Node.state;
|
||||
}
|
||||
|
||||
// Node-type module to extend the Node instance with. Setting of this is
|
||||
// done by canopy during parsing and is setup in [parser.js](./parser.html).
|
||||
set module(mod) {
|
||||
_.extend(this, mod);
|
||||
|
||||
if (this.setup) {
|
||||
this.setup();
|
||||
}
|
||||
|
||||
_.forOwn(this.definedProperties || {}, (methods, name) => {
|
||||
Object.defineProperty(this, name, methods);
|
||||
});
|
||||
|
||||
delete this.definedProperties;
|
||||
}
|
||||
|
||||
// The SVG element to render this node into. A node-type class is
|
||||
// automatically added to the container. The class to set is defined on the
|
||||
// module set during parsing.
|
||||
set container(container) {
|
||||
this._container = container;
|
||||
this._container.addClass(this.type);
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
// The anchor defined the points on the left and right of the rendered node
|
||||
// that the centerline of the rendered expression connects to. For most
|
||||
// nodes, this element will be defined by the normalizeBBox method in
|
||||
// [Util](../../util.html).
|
||||
get anchor() {
|
||||
if (this.proxy) {
|
||||
return this.proxy.anchor;
|
||||
} else {
|
||||
return this._anchor || {};
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the bounding box of the container with the anchor included.
|
||||
getBBox() {
|
||||
return _.extend(util.normalizeBBox(this.container.getBBox()), this.anchor);
|
||||
}
|
||||
|
||||
// Transforms the container.
|
||||
//
|
||||
// - __matrix__ - A matrix transform to be applied. Created using Snap.svg.
|
||||
transform(matrix) {
|
||||
return this.container.transform(matrix);
|
||||
}
|
||||
|
||||
// Returns a Promise that will be resolved with the provided value. If the
|
||||
// render is cancelled before the Promise is resolved, then an exception will
|
||||
// be thrown to halt any rendering.
|
||||
//
|
||||
// - __value__ - Value to resolve the returned promise with.
|
||||
deferredStep(value) {
|
||||
return util.tick().then(() => {
|
||||
if (this.state.cancelRender) {
|
||||
throw 'Render cancelled';
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
// Render this node.
|
||||
//
|
||||
// - __container__ - Optional element to render this node into. A container
|
||||
// must be specified, but if it has already been set, then it does not
|
||||
// need to be provided to render.
|
||||
render(container) {
|
||||
if (container) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
if (this.proxy) {
|
||||
// For nodes that proxy to a child node, just render the child.
|
||||
return this.proxy.render(this.container);
|
||||
} else {
|
||||
// Non-proxied nodes call their _render method (defined by the node-type
|
||||
// module).
|
||||
this.state.renderCounter++;
|
||||
return this._render()
|
||||
.then(() => {
|
||||
this.state.renderCounter--;
|
||||
return this;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Renders a label centered within a rectangle which can be styled. Returns
|
||||
// a Promise which will be resolved with the SVG group the rect and text are
|
||||
// rendered in.
|
||||
//
|
||||
// - __text__ - String or array of strings to render as a label.
|
||||
renderLabel(text) {
|
||||
let group = this.container.group()
|
||||
.addClass('label'),
|
||||
rect = group.rect(),
|
||||
label = group.text(0, 0, _.flatten([text]));
|
||||
|
||||
return this.deferredStep()
|
||||
.then(() => {
|
||||
let box = label.getBBox(),
|
||||
margin = 5;
|
||||
|
||||
label.transform(Snap.matrix()
|
||||
.translate(margin, box.height / 2 + 2 * margin));
|
||||
|
||||
rect.attr({
|
||||
width: box.width + 2 * margin,
|
||||
height: box.height + 2 * margin
|
||||
});
|
||||
|
||||
return group;
|
||||
});
|
||||
}
|
||||
|
||||
// Renders a labeled box around another SVG element. Returns a Promise.
|
||||
//
|
||||
// - __text__ - String or array of strings to label the box with.
|
||||
// - __content__ - SVG element to wrap in the box.
|
||||
// - __options.padding__ - Pixels of padding to place between the content and
|
||||
// the box.
|
||||
renderLabeledBox(text, content, options) {
|
||||
let label = this.container.text(0, 0, _.flatten([text]))
|
||||
.addClass(`${this.type}-label`),
|
||||
box = this.container.rect()
|
||||
.addClass(`${this.type}-box`)
|
||||
.attr({
|
||||
rx: 3,
|
||||
ry: 3
|
||||
});
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
padding: 0
|
||||
});
|
||||
|
||||
this.container.prepend(label);
|
||||
this.container.prepend(box);
|
||||
|
||||
return this.deferredStep()
|
||||
.then(() => {
|
||||
let labelBox = label.getBBox(),
|
||||
contentBox = content.getBBox(),
|
||||
boxWidth = Math.max(contentBox.width + options.padding * 2, labelBox.width),
|
||||
boxHeight = contentBox.height + options.padding * 2;
|
||||
|
||||
label.transform(Snap.matrix()
|
||||
.translate(0, labelBox.height));
|
||||
|
||||
box
|
||||
.transform(Snap.matrix()
|
||||
.translate(0, labelBox.height))
|
||||
.attr({
|
||||
width: boxWidth,
|
||||
height: boxHeight
|
||||
});
|
||||
|
||||
content.transform(Snap.matrix()
|
||||
.translate(boxWidth / 2 - contentBox.cx, labelBox.height + options.padding));
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
// Sets up the parser generated by canopy to use the
|
||||
// [Node](./javascript/node.html) subclasses in the generated tree. This is all
|
||||
// a bit of a hack that is dependent on how canopy creates nodes in its parse
|
||||
// tree.
|
||||
import parser from './grammar.peg';
|
||||
|
||||
import Node from './node.js';
|
||||
import Root from './root.js';
|
||||
import Regexp from './regexp.js';
|
||||
import Match from './match.js';
|
||||
import MatchFragment from './match_fragment.js';
|
||||
import Anchor from './anchor.js';
|
||||
import Subexp from './subexp.js';
|
||||
import Charset from './charset.js';
|
||||
import CharsetEscape from './charset_escape.js';
|
||||
import CharsetRange from './charset_range.js';
|
||||
import Literal from './literal.js';
|
||||
import Escape from './escape.js';
|
||||
import AnyCharacter from './any_character.js';
|
||||
import Repeat from './repeat.js';
|
||||
import RepeatAny from './repeat_any.js';
|
||||
import RepeatOptional from './repeat_optional.js';
|
||||
import RepeatRequired from './repeat_required.js';
|
||||
import RepeatSpec from './repeat_spec.js';
|
||||
|
||||
// Canopy creates an instance of SyntaxNode for each element in the tree, then
|
||||
// adds any necessary fields to that instance. In this case, we're replacing
|
||||
// the default class with the Node class.
|
||||
parser.Parser.SyntaxNode = Node;
|
||||
|
||||
// Once the SyntaxNode instance is created, the specific node type object is
|
||||
// overlayed onto it. This causes the module attribute on the Node to be set,
|
||||
// which updates the Node instance into the more specific "subclass" that is
|
||||
// used for rendering.
|
||||
parser.Parser.Root = { module: Root };
|
||||
parser.Parser.Regexp = { module: Regexp };
|
||||
parser.Parser.Match = { module: Match };
|
||||
parser.Parser.MatchFragment = { module: MatchFragment };
|
||||
parser.Parser.Anchor = { module: Anchor };
|
||||
parser.Parser.Subexp = { module: Subexp };
|
||||
parser.Parser.Charset = { module: Charset };
|
||||
parser.Parser.CharsetEscape = { module: CharsetEscape };
|
||||
parser.Parser.CharsetRange = { module: CharsetRange };
|
||||
parser.Parser.Literal = { module: Literal };
|
||||
parser.Parser.Escape = { module: Escape };
|
||||
parser.Parser.AnyCharacter = { module: AnyCharacter };
|
||||
parser.Parser.Repeat = { module: Repeat };
|
||||
parser.Parser.RepeatAny = { module: RepeatAny };
|
||||
parser.Parser.RepeatOptional = { module: RepeatOptional };
|
||||
parser.Parser.RepeatRequired = { module: RepeatRequired };
|
||||
parser.Parser.RepeatSpec = { module: RepeatSpec };
|
||||
|
||||
export default parser;
|
||||
@@ -1,36 +0,0 @@
|
||||
// State tracking for an in-progress parse and render.
|
||||
export default class ParserState {
|
||||
// - __progress__ - DOM node to update to indicate completion progress.
|
||||
constructor(progress) {
|
||||
// Tracks the number of capture groups in the expression.
|
||||
this.groupCounter = 1;
|
||||
// Cancels the in-progress render when set to true.
|
||||
this.cancelRender = false;
|
||||
// Warnings that have been generated while rendering.
|
||||
this.warnings = [];
|
||||
|
||||
// Used to display the progress indicator
|
||||
this._renderCounter = 0;
|
||||
this._maxCounter = 0;
|
||||
this._progress = progress;
|
||||
}
|
||||
|
||||
// Counts the number of in-progress rendering steps. As the counter goes up,
|
||||
// a maximum value is also tracked. The maximum value and current render
|
||||
// counter are used to calculate the completion process.
|
||||
get renderCounter() {
|
||||
return this._renderCounter;
|
||||
}
|
||||
|
||||
set renderCounter(value) {
|
||||
if (value > this.renderCounter) {
|
||||
this._maxCounter = value;
|
||||
}
|
||||
|
||||
this._renderCounter = value;
|
||||
|
||||
if (this._maxCounter && !this.cancelRender) {
|
||||
this._progress.style.width = ((1 - this.renderCounter / this._maxCounter) * 100).toFixed(2) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Regexp nodes are the entire regular expression. They consist of a collection
|
||||
// of [Match](./match.html) nodes separated by `|`.
|
||||
|
||||
import util from '../../util.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'regexp',
|
||||
|
||||
// Renders the regexp into the currently set container.
|
||||
_render() {
|
||||
let matchContainer = this.container.group()
|
||||
.addClass('regexp-matches')
|
||||
.transform(Snap.matrix()
|
||||
.translate(20, 0));
|
||||
|
||||
// Renders each match into the match container.
|
||||
return Promise.all(_.map(this.matches,
|
||||
match => match.render(matchContainer.group())
|
||||
))
|
||||
.then(() => {
|
||||
let containerBox,
|
||||
paths;
|
||||
|
||||
// Space matches vertically in the match container.
|
||||
util.spaceVertically(this.matches, {
|
||||
padding: 5
|
||||
});
|
||||
|
||||
containerBox = this.getBBox();
|
||||
|
||||
// Creates the curves from the side lines for each match.
|
||||
paths = _.map(this.matches, match => this.makeCurve(containerBox, match));
|
||||
|
||||
// Add side lines to the list of paths.
|
||||
paths.push(this.makeSide(containerBox, _.first(this.matches)));
|
||||
paths.push(this.makeSide(containerBox, _.last(this.matches)));
|
||||
|
||||
// Render connector paths.
|
||||
this.container.prepend(
|
||||
this.container.path(_(paths).flatten().compact().values().join('')));
|
||||
|
||||
containerBox = matchContainer.getBBox();
|
||||
|
||||
// Create connections from side lines to each match and render into
|
||||
// the match container.
|
||||
paths = _.map(this.matches, match => this.makeConnector(containerBox, match));
|
||||
matchContainer.prepend(
|
||||
matchContainer.path(paths.join('')));
|
||||
});
|
||||
},
|
||||
|
||||
// Returns an array of SVG path strings to draw the vertical lines on the
|
||||
// left and right of the node.
|
||||
//
|
||||
// - __containerBox__ - Bounding box of the container.
|
||||
// - __match__ - Match node that the line will be drawn to.
|
||||
makeSide(containerBox, match) {
|
||||
let box = match.getBBox(),
|
||||
distance = Math.abs(box.ay - containerBox.cy);
|
||||
|
||||
// Only need to draw side lines if the match is more than 15 pixels from
|
||||
// the vertical center of the rendered regexp. Less that 15 pixels will be
|
||||
// handled by the curve directly.
|
||||
if (distance >= 15) {
|
||||
let shift = (box.ay > containerBox.cy) ? 10 : -10,
|
||||
edge = box.ay - shift;
|
||||
|
||||
return [
|
||||
`M0,${containerBox.cy}q10,0 10,${shift}V${edge}`,
|
||||
`M${containerBox.width + 40},${containerBox.cy}q-10,0 -10,${shift}V${edge}`
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
// Returns an array of SVG path strings to draw the curves from the
|
||||
// sidelines up to the anchor of the match node.
|
||||
//
|
||||
// - __containerBox__ - Bounding box of the container.
|
||||
// - __match__ - Match node that the line will be drawn to.
|
||||
makeCurve(containerBox, match) {
|
||||
let box = match.getBBox(),
|
||||
distance = Math.abs(box.ay - containerBox.cy);
|
||||
|
||||
if (distance >= 15) {
|
||||
// For match nodes more than 15 pixels from the center of the regexp, a
|
||||
// quarter-circle curve is used to connect to the sideline.
|
||||
let curve = (box.ay > containerBox.cy) ? 10 : -10;
|
||||
|
||||
return [
|
||||
`M10,${box.ay - curve}q0,${curve} 10,${curve}`,
|
||||
`M${containerBox.width + 30},${box.ay - curve}q0,${curve} -10,${curve}`
|
||||
];
|
||||
} else {
|
||||
// For match nodes less than 15 pixels from the center of the regexp, a
|
||||
// slightly curved line is used to connect to the sideline.
|
||||
let anchor = box.ay - containerBox.cy;
|
||||
|
||||
return [
|
||||
`M0,${containerBox.cy}c10,0 10,${anchor} 20,${anchor}`,
|
||||
`M${containerBox.width + 40},${containerBox.cy}c-10,0 -10,${anchor} -20,${anchor}`
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
// Returns an array of SVG path strings to draw the connection from the
|
||||
// curve to match node.
|
||||
//
|
||||
// - __containerBox__ - Bounding box of the container.
|
||||
// - __match__ - Match node that the line will be drawn to.
|
||||
makeConnector(containerBox, match) {
|
||||
let box = match.getBBox();
|
||||
|
||||
return `M0,${box.ay}h${box.ax}M${box.ax2},${box.ay}H${containerBox.width}`;
|
||||
},
|
||||
|
||||
setup() {
|
||||
if (this.properties.alternates.elements.length === 0) {
|
||||
// When there is only one match node to render, proxy to it.
|
||||
this.proxy = this.properties.match;
|
||||
} else {
|
||||
// Merge all the match nodes into one array.
|
||||
this.matches = [this.properties.match].concat(
|
||||
_.map(this.properties.alternates.elements,
|
||||
element => element.properties.match)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,122 +0,0 @@
|
||||
// Repeat nodes are for the various repetition syntaxes (`a*`, `a+`, `a?`, and
|
||||
// `a{1,3}`). It is not rendered directly, but contains data used for the
|
||||
// rendering of [MatchFragment](./match_fragment.html) nodes.
|
||||
|
||||
function formatTimes(times) {
|
||||
if (times === 1) {
|
||||
return 'once';
|
||||
} else {
|
||||
return `${times} times`;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
definedProperties: {
|
||||
// Translation to apply to content to be repeated to account for the loop
|
||||
// and skip lines.
|
||||
contentPosition: {
|
||||
get: function() {
|
||||
var matrix = Snap.matrix();
|
||||
|
||||
if (this.hasSkip) {
|
||||
return matrix.translate(15, 10);
|
||||
} else if (this.hasLoop) {
|
||||
return matrix.translate(10, 0);
|
||||
} else {
|
||||
return matrix.translate(0, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Label to place of loop path to indicate the number of times that path
|
||||
// may be followed.
|
||||
label: {
|
||||
get: function() {
|
||||
if (this.minimum === this.maximum) {
|
||||
if (this.minimum === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return formatTimes(this.minimum - 1);
|
||||
} else if (this.minimum <= 1 && this.maximum >= 2) {
|
||||
return `at most ${formatTimes(this.maximum - 1)}`;
|
||||
} else if (this.minimum >= 2) {
|
||||
if (this.maximum === -1) {
|
||||
return `${this.minimum - 1}+ times`;
|
||||
} else {
|
||||
return `${this.minimum - 1}\u2026${formatTimes(this.maximum - 1)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Tooltip to place of loop path label to provide further details.
|
||||
tooltip: {
|
||||
get: function() {
|
||||
let repeatCount;
|
||||
if (this.minimum === this.maximum) {
|
||||
if (this.minimum === 0) {
|
||||
repeatCount = undefined;
|
||||
} else {
|
||||
repeatCount = formatTimes(this.minimum);
|
||||
}
|
||||
} else if (this.minimum <= 1 && this.maximum >= 2) {
|
||||
repeatCount = `at most ${formatTimes(this.maximum)}`;
|
||||
} else if (this.minimum >= 2) {
|
||||
if (this.maximum === -1) {
|
||||
repeatCount = `${this.minimum}+ times`;
|
||||
} else {
|
||||
repeatCount = `${this.minimum}\u2026${formatTimes(this.maximum)}`;
|
||||
}
|
||||
}
|
||||
return repeatCount ? `repeats ${repeatCount} in total` : repeatCount;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the path spec to render the line that skips over the content for
|
||||
// fragments that are optionally matched.
|
||||
skipPath(box) {
|
||||
let paths = [];
|
||||
|
||||
if (this.hasSkip) {
|
||||
let vert = Math.max(0, box.ay - box.y - 10),
|
||||
horiz = box.width - 10;
|
||||
|
||||
paths.push(`M0,${box.ay}q10,0 10,-10v${-vert}q0,-10 10,-10h${horiz}q10,0 10,10v${vert}q0,10 10,10`);
|
||||
|
||||
// When the repeat is not greedy, the skip path gets a preference arrow.
|
||||
if (!this.greedy) {
|
||||
paths.push(`M10,${box.ay - 15}l5,5m-5,-5l-5,5`);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
},
|
||||
|
||||
// Returns the path spec to render the line that repeats the content for
|
||||
// fragments that are matched more than once.
|
||||
loopPath(box) {
|
||||
let paths = [];
|
||||
|
||||
if (this.hasLoop) {
|
||||
let vert = box.y2 - box.ay - 10;
|
||||
|
||||
paths.push(`M${box.x},${box.ay}q-10,0 -10,10v${vert}q0,10 10,10h${box.width}q10,0 10,-10v${-vert}q0,-10 -10,-10`);
|
||||
|
||||
// When the repeat is greedy, the loop path gets the preference arrow.
|
||||
if (this.greedy) {
|
||||
paths.push(`M${box.x2 + 10},${box.ay + 15}l5,-5m-5,5l-5,-5`);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
},
|
||||
|
||||
setup() {
|
||||
this.minimum = this.properties.spec.minimum;
|
||||
this.maximum = this.properties.spec.maximum;
|
||||
this.greedy = (this.properties.greedy.textValue === '');
|
||||
this.hasSkip = (this.minimum === 0);
|
||||
this.hasLoop = (this.maximum === -1 || this.maximum > 1);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// RepeatAny nodes are used for `a*` regular expression syntax. It is not
|
||||
// rendered directly; it just indicates that the [Repeat](./repeat.html) node
|
||||
// loops zero or more times.
|
||||
|
||||
export default {
|
||||
minimum: 0,
|
||||
maximum: -1
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
// RepeatOptional nodes are used for `a?` regular expression syntax. It is not
|
||||
// rendered directly; it just indicates that the [Repeat](./repeat.html) node
|
||||
// loops zero or one times.
|
||||
|
||||
export default {
|
||||
minimum: 0,
|
||||
maximum: 1
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
// RepeatRequired nodes are used for `a+` regular expression syntax. It is not
|
||||
// rendered directly; it just indicates that the [Repeat](./repeat.html) node
|
||||
// loops one or more times.
|
||||
|
||||
export default {
|
||||
minimum: 1,
|
||||
maximum: -1
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
// RepeatSpec nodes are used for `a{m,n}` regular expression syntax. It is not
|
||||
// rendered directly; it just indicates how many times the
|
||||
// [Repeat](./repeat.html) node loops.
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
if (this.properties.min) {
|
||||
this.minimum = Number(this.properties.min.textValue);
|
||||
} else if (this.properties.exact) {
|
||||
this.minimum = Number(this.properties.exact.textValue);
|
||||
} else {
|
||||
this.minimum = 0;
|
||||
}
|
||||
|
||||
if (this.properties.max) {
|
||||
this.maximum = Number(this.properties.max.textValue);
|
||||
} else if (this.properties.exact) {
|
||||
this.maximum = Number(this.properties.exact.textValue);
|
||||
} else {
|
||||
this.maximum = -1;
|
||||
}
|
||||
|
||||
// Report invalid repeat when the minimum is larger than the maximum.
|
||||
if (this.minimum > this.maximum && this.maximum !== -1) {
|
||||
throw `Numbers out of order: ${this.textValue}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
// Root nodes contain the top-level [Regexp](./regexp.html) node. Any flags
|
||||
// and a few decorative elements are rendered by the root node.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'root',
|
||||
|
||||
flagLabels: {
|
||||
i: 'Ignore Case',
|
||||
g: 'Global',
|
||||
m: 'Multiline',
|
||||
y: 'Sticky',
|
||||
u: 'Unicode'
|
||||
},
|
||||
|
||||
// Renders the root into the currently set container.
|
||||
_render() {
|
||||
let flagText;
|
||||
|
||||
// Render a label for any flags that have been set of the expression.
|
||||
if (this.flags.length > 0) {
|
||||
flagText = this.container.text(0, 0, `Flags: ${this.flags.join(', ')}`);
|
||||
}
|
||||
|
||||
// Render the content of the regular expression.
|
||||
return this.regexp.render(this.container.group())
|
||||
.then(() => {
|
||||
// Move rendered regexp to account for flag label and to allow for
|
||||
// decorative elements.
|
||||
if (flagText) {
|
||||
this.regexp.transform(Snap.matrix()
|
||||
.translate(10, flagText.getBBox().height));
|
||||
} else {
|
||||
this.regexp.transform(Snap.matrix()
|
||||
.translate(10, 0));
|
||||
}
|
||||
|
||||
let box = this.regexp.getBBox();
|
||||
|
||||
// Render decorative elements.
|
||||
this.container.path(`M${box.ax},${box.ay}H0M${box.ax2},${box.ay}H${box.x2 + 10}`);
|
||||
this.container.circle(0, box.ay, 5);
|
||||
this.container.circle(box.x2 + 10, box.ay, 5);
|
||||
});
|
||||
},
|
||||
|
||||
setup() {
|
||||
// Convert list of flags into text describing each flag.
|
||||
this.flags = _(this.properties.flags.textValue)
|
||||
.uniq().sort()
|
||||
.map(flag => this.flagLabels[flag]).value();
|
||||
|
||||
this.regexp = this.properties.regexp
|
||||
}
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
// Subexp nodes are for expressions inside of parenthesis. It is rendered as a
|
||||
// labeled box around the contained expression if a label is required.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
type: 'subexp',
|
||||
|
||||
definedProperties: {
|
||||
// Default anchor is overridden to move it down to account for the group
|
||||
// label and outline box.
|
||||
_anchor: {
|
||||
get: function() {
|
||||
var anchor = this.regexp.getBBox(),
|
||||
matrix = this.transform().localMatrix;
|
||||
|
||||
return {
|
||||
ax: matrix.x(anchor.ax, anchor.ay),
|
||||
ax2: matrix.x(anchor.ax2, anchor.ay),
|
||||
ay: matrix.y(anchor.ax, anchor.ay)
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
labelMap: {
|
||||
'?:': '',
|
||||
'?=': 'positive lookahead',
|
||||
'?!': 'negative lookahead'
|
||||
},
|
||||
|
||||
// Renders the subexp into the currently set container.
|
||||
_render() {
|
||||
// **NOTE:** `this.label()` **MUST** be called here, in _render, and before
|
||||
// any child nodes are rendered. This is to keep the group numbers in the
|
||||
// correct order.
|
||||
let label = this.label();
|
||||
|
||||
// Render the contained regexp.
|
||||
return this.regexp.render(this.container.group())
|
||||
// Create the labeled box around the regexp.
|
||||
.then(() => this.renderLabeledBox(label, this.regexp, {
|
||||
padding: 10
|
||||
}));
|
||||
},
|
||||
|
||||
// Returns the label for the subexpression.
|
||||
label() {
|
||||
if (_.has(this.labelMap, this.properties.capture.textValue)) {
|
||||
return this.labelMap[this.properties.capture.textValue];
|
||||
} else {
|
||||
return `group #${this.state.groupCounter++}`;
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
// **NOTE:** **DO NOT** call `this.label()` in setup. It will lead to
|
||||
// groups being numbered in reverse order.
|
||||
this.regexp = this.properties.regexp;
|
||||
|
||||
// If there is no need for a label, then proxy to the nested regexp.
|
||||
if (this.properties.capture.textValue == '?:') {
|
||||
this.proxy = this.regexp;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,290 +0,0 @@
|
||||
// The Regexper class manages the top-level behavior for the entire
|
||||
// application. This includes event handlers for all user interactions.
|
||||
|
||||
import util from './util.js';
|
||||
import Parser from './parser/javascript.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class Regexper {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.buggyHash = false;
|
||||
this.form = root.querySelector('#regexp-form');
|
||||
this.field = root.querySelector('#regexp-input');
|
||||
this.error = root.querySelector('#error');
|
||||
this.warnings = root.querySelector('#warnings');
|
||||
|
||||
this.links = this.form.querySelector('ul');
|
||||
this.permalink = this.links.querySelector('a[data-action="permalink"]');
|
||||
this.downloadSvg = this.links.querySelector('a[data-action="download-svg"]');
|
||||
this.downloadPng = this.links.querySelector('a[data-action="download-png"]');
|
||||
|
||||
this.svgContainer = root.querySelector('#regexp-render');
|
||||
}
|
||||
|
||||
// Event handler for key presses in the regular expression form field.
|
||||
keypressListener(event) {
|
||||
// Pressing Shift-Enter displays the expression.
|
||||
if (event.shiftKey && event.keyCode === 13) {
|
||||
event.returnValue = false;
|
||||
if (event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.form.dispatchEvent(util.customEvent('submit'));
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for key presses while focused anywhere in the application.
|
||||
documentKeypressListener(event) {
|
||||
// Pressing escape will cancel a currently running render.
|
||||
if (event.keyCode === 27 && this.running) {
|
||||
this.running.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for submission of the regular expression. Changes the URL
|
||||
// hash which leads to the expression being rendered.
|
||||
submitListener(event) {
|
||||
event.returnValue = false;
|
||||
if (event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
try {
|
||||
this._setHash(this.field.value);
|
||||
}
|
||||
catch(e) {
|
||||
// Failed to set the URL hash (probably because the expression is too
|
||||
// long). Turn off display of the permalink and just show the expression.
|
||||
this.permalinkEnabled = false;
|
||||
this.showExpression(this.field.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for URL hash changes. Starts rendering of the expression.
|
||||
hashchangeListener() {
|
||||
let expr = this._getHash();
|
||||
|
||||
if (expr instanceof Error) {
|
||||
this.state = 'has-error';
|
||||
this.error.innerHTML = 'Malformed expression in URL';
|
||||
util.track('send', 'event', 'visualization', 'malformed URL');
|
||||
} else {
|
||||
this.permalinkEnabled = true;
|
||||
this.showExpression(expr);
|
||||
}
|
||||
}
|
||||
|
||||
// Binds all event listeners.
|
||||
bindListeners() {
|
||||
this.field.addEventListener('keypress', this.keypressListener.bind(this));
|
||||
this.form.addEventListener('submit', this.submitListener.bind(this));
|
||||
this.root.addEventListener('keyup', this.documentKeypressListener.bind(this));
|
||||
window.addEventListener('hashchange', this.hashchangeListener.bind(this));
|
||||
}
|
||||
|
||||
// Detect if https://bugzilla.mozilla.org/show_bug.cgi?id=483304 is in effect
|
||||
detectBuggyHash() {
|
||||
if (typeof window.URL === 'function') {
|
||||
try {
|
||||
let url = new URL('http://regexper.com/#%25');
|
||||
this.buggyHash = (url.hash === '#%');
|
||||
}
|
||||
catch(e) {
|
||||
this.buggyHash = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the URL hash. This method exists to facilitate automated testing
|
||||
// (since changing the URL can throw off most JavaScript testing tools).
|
||||
_setHash(hash) {
|
||||
location.hash = encodeURIComponent(hash)
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29');
|
||||
}
|
||||
|
||||
// Retrieve the current URL hash. This method is also mostly for supporting
|
||||
// automated testing, but also does some basic error handling for malformed
|
||||
// URLs.
|
||||
_getHash() {
|
||||
try {
|
||||
let hash = location.hash.slice(1)
|
||||
return this.buggyHash ? hash : decodeURIComponent(hash);
|
||||
}
|
||||
catch(e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently state of the application. Useful values are:
|
||||
// - `''` - State of the application when the page initially loads
|
||||
// - `'is-loading'` - Displays the loading indicator
|
||||
// - `'has-error'` - Displays the error message
|
||||
// - `'has-results'` - Displays rendered results
|
||||
set state(state) {
|
||||
this.root.className = state;
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this.root.className;
|
||||
}
|
||||
|
||||
// Start the rendering of a regular expression.
|
||||
//
|
||||
// - __expression__ - Regular expression to display.
|
||||
showExpression(expression) {
|
||||
this.field.value = expression;
|
||||
this.state = '';
|
||||
|
||||
if (expression !== '') {
|
||||
this.renderRegexp(expression).catch(util.exposeError);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a blob URL for linking to a rendered regular expression image.
|
||||
//
|
||||
// - __content__ - SVG image markup.
|
||||
buildBlobURL(content) {
|
||||
// Blob object has to stick around for IE, so the instance is stored on the
|
||||
// `window` object.
|
||||
window.blob = new Blob([content], { type: 'image/svg+xml' });
|
||||
return URL.createObjectURL(window.blob);
|
||||
}
|
||||
|
||||
// Update the URLs of the 'download' and 'permalink' links.
|
||||
updateLinks() {
|
||||
let classes = _.without(this.links.className.split(' '), ['hide-download-svg', 'hide-permalink']);
|
||||
let svg = this.svgContainer.querySelector('.svg');
|
||||
|
||||
// Create the SVG 'download' image URL.
|
||||
try {
|
||||
this.downloadSvg.parentNode.style.display = null;
|
||||
this.downloadSvg.href = this.buildBlobURL(svg.innerHTML);
|
||||
}
|
||||
catch(e) {
|
||||
// Blobs or URLs created from a blob URL don't work in the current
|
||||
// browser. Giving up on the download link.
|
||||
classes.push('hide-download-svg');
|
||||
}
|
||||
|
||||
//Create the PNG 'download' image URL.
|
||||
try {
|
||||
let canvas = document.createElement('canvas');
|
||||
let context = canvas.getContext('2d');
|
||||
let loader = new Image;
|
||||
|
||||
loader.width = canvas.width = Number(svg.querySelector('svg').getAttribute('width'));
|
||||
loader.height = canvas.height = Number(svg.querySelector('svg').getAttribute('height'));
|
||||
loader.onload = () => {
|
||||
try {
|
||||
context.drawImage(loader, 0, 0, loader.width, loader.height);
|
||||
canvas.toBlob(blob => {
|
||||
window.pngBlob = blob;
|
||||
this.downloadPng.href = URL.createObjectURL(window.pngBlob);
|
||||
this.links.className = this.links.className.replace(/\bhide-download-png\b/, '');
|
||||
}, 'image/png');
|
||||
}
|
||||
catch(e) {}
|
||||
};
|
||||
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svg.innerHTML);
|
||||
classes.push('hide-download-png');
|
||||
}
|
||||
catch(e) {}
|
||||
|
||||
// Create the 'permalink' URL.
|
||||
if (this.permalinkEnabled) {
|
||||
this.permalink.parentNode.style.display = null;
|
||||
this.permalink.href = location.toString();
|
||||
} else {
|
||||
classes.push('hide-permalink');
|
||||
}
|
||||
|
||||
this.links.className = classes.join(' ');
|
||||
}
|
||||
|
||||
// Display any warnings that were generated while rendering a regular expression.
|
||||
//
|
||||
// - __warnings__ - Array of warning messages to display.
|
||||
displayWarnings(warnings) {
|
||||
this.warnings.innerHTML = _.map(warnings, warning => (
|
||||
`<li class="inline-icon">${util.icon("#warning")}${warning}</li>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
// Render regular expression
|
||||
//
|
||||
// - __expression__ - Regular expression to render
|
||||
renderRegexp(expression) {
|
||||
let parseError = false,
|
||||
startTime, endTime;
|
||||
|
||||
// When a render is already in progress, cancel it and try rendering again
|
||||
// after a short delay (canceling a render is not instantaneous).
|
||||
if (this.running) {
|
||||
this.running.cancel();
|
||||
|
||||
return util.wait(10).then(() => this.renderRegexp(expression));
|
||||
}
|
||||
|
||||
this.state = 'is-loading';
|
||||
util.track('send', 'event', 'visualization', 'start');
|
||||
startTime = new Date().getTime();
|
||||
|
||||
this.running = new Parser(this.svgContainer);
|
||||
|
||||
return this.running
|
||||
// Parse the expression.
|
||||
.parse(expression)
|
||||
// Display any error messages from the parser and abort the render.
|
||||
.catch(message => {
|
||||
this.state = 'has-error';
|
||||
this.error.innerHTML = '';
|
||||
this.error.appendChild(document.createTextNode(message));
|
||||
|
||||
parseError = true;
|
||||
|
||||
throw message;
|
||||
})
|
||||
// When parsing is successful, render the parsed expression.
|
||||
.then(parser => parser.render())
|
||||
// Once rendering is complete:
|
||||
// - Update links
|
||||
// - Display any warnings
|
||||
// - Track the completion of the render and how long it took
|
||||
.then(() => {
|
||||
this.state = 'has-results';
|
||||
this.updateLinks();
|
||||
this.displayWarnings(this.running.warnings);
|
||||
util.track('send', 'event', 'visualization', 'complete');
|
||||
|
||||
endTime = new Date().getTime();
|
||||
util.track('send', 'timing', 'visualization', 'total time', endTime - startTime);
|
||||
})
|
||||
// Handle any errors that happened during the rendering pipeline.
|
||||
// Swallows parse errors and render cancellations. Any other exceptions
|
||||
// are allowed to continue on to be tracked by the global error handler.
|
||||
.catch(message => {
|
||||
if (message === 'Render cancelled') {
|
||||
util.track('send', 'event', 'visualization', 'cancelled');
|
||||
this.state = '';
|
||||
} else if (parseError) {
|
||||
util.track('send', 'event', 'visualization', 'parse error');
|
||||
} else {
|
||||
throw message;
|
||||
}
|
||||
})
|
||||
// Finally, mark rendering as complete (and pass along any exceptions
|
||||
// that were thrown).
|
||||
.then(
|
||||
() => {
|
||||
this.running = false;
|
||||
},
|
||||
message => {
|
||||
this.running = false;
|
||||
throw message;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
// Utility functions used elsewhere in the codebase. Most JavaScript files on
|
||||
// the site use some functions defined in this file.
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
// Generate an `Event` object for triggering a custom event.
|
||||
//
|
||||
// - __name__ - Name of the custom event. This should be a String.
|
||||
// - __detail__ - Event details. The event details are provided to the event
|
||||
// handler.
|
||||
function customEvent(name, detail) {
|
||||
var evt = document.createEvent('Event');
|
||||
evt.initEvent(name, true, true);
|
||||
evt.detail = detail;
|
||||
return evt;
|
||||
}
|
||||
|
||||
// Add extra fields to a bounding box returned by `getBBox`. Specifically adds
|
||||
// details about the box's axis points (used when positioning elements for
|
||||
// display).
|
||||
//
|
||||
// - __box__ - Bounding box object to update. Attributes `ax`, `ax2`, and `ay`
|
||||
// will be added if they are not already defined.
|
||||
function normalizeBBox(box) {
|
||||
return _.defaults(box, {
|
||||
ax: box.x,
|
||||
ax2: box.x2,
|
||||
ay: box.cy
|
||||
});
|
||||
}
|
||||
|
||||
// Positions a collection of items with their axis points aligned along a
|
||||
// horizontal line. This leads to the items being spaced horizontally and
|
||||
// effectively centered vertically.
|
||||
//
|
||||
// - __items__ - Array of items to be positioned
|
||||
// - __options.padding__ - Number of pixels to leave between items
|
||||
function spaceHorizontally(items, options) {
|
||||
var verticalCenter,
|
||||
values;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
padding: 0
|
||||
});
|
||||
|
||||
values = _.map(items, item => ({
|
||||
box: normalizeBBox(item.getBBox()),
|
||||
item
|
||||
}));
|
||||
|
||||
// Calculate where the axis points should be positioned vertically.
|
||||
verticalCenter = _.reduce(values,
|
||||
(center, { box }) => Math.max(center, box.ay),
|
||||
0);
|
||||
|
||||
// Position items with padding between them and aligned their axis points.
|
||||
_.reduce(values, (offset, { item, box }) => {
|
||||
item.transform(Snap.matrix()
|
||||
.translate(offset, verticalCenter - box.ay));
|
||||
|
||||
return offset + options.padding + box.width;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Positions a collection of items centered horizontally in a vertical stack.
|
||||
//
|
||||
// - __items__ - Array of items to be positioned
|
||||
// - __options.padding__ - Number of pixels to leave between items
|
||||
function spaceVertically(items, options) {
|
||||
var horizontalCenter,
|
||||
values;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
padding: 0
|
||||
});
|
||||
|
||||
values = _.map(items, item => ({
|
||||
box: item.getBBox(),
|
||||
item
|
||||
}));
|
||||
|
||||
// Calculate where the center of each item should be positioned horizontally.
|
||||
horizontalCenter = _.reduce(values,
|
||||
(center, { box }) => Math.max(center, box.cx),
|
||||
0);
|
||||
|
||||
// Position items with padding between them and align their centers.
|
||||
_.reduce(values, (offset, { item, box }) => {
|
||||
item.transform(Snap.matrix()
|
||||
.translate(horizontalCenter - box.cx, offset));
|
||||
|
||||
return offset + options.padding + box.height;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Creates a Promise that will be resolved after a specified delay.
|
||||
//
|
||||
// - __delay__ - Time in milliseconds to wait before resolving promise.
|
||||
function wait(delay) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a Promise that will be resolved after 0 milliseconds. This is used
|
||||
// to create a short delay that allows the browser to address any pending tasks
|
||||
// while the JavaScript VM is not active.
|
||||
function tick() {
|
||||
return wait(0);
|
||||
}
|
||||
|
||||
// Re-throws an exception asynchronously. This is used to expose an exception
|
||||
// that was created during a Promise operation to be handled by global error
|
||||
// handlers (and to be displayed in the browser's debug console).
|
||||
//
|
||||
// - __error__ - Error/exception object to be re-thrown to the browser.
|
||||
function exposeError(error) {
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Renders an SVG icon.
|
||||
//
|
||||
// - __selector__ - Selector to the SVG icon to render.
|
||||
function icon(selector) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 8 8"><use xlink:href="${selector}" /></svg>`;
|
||||
}
|
||||
|
||||
// Send tracking data.
|
||||
function track() {
|
||||
if (window.ga) {
|
||||
ga.apply(ga, arguments);
|
||||
} else {
|
||||
console.debug.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
customEvent,
|
||||
normalizeBBox,
|
||||
spaceHorizontally,
|
||||
spaceVertically,
|
||||
wait,
|
||||
tick,
|
||||
exposeError,
|
||||
icon,
|
||||
track
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
# robotstxt.org/
|
||||
|
||||
User-agent: *
|
||||
@@ -1,50 +0,0 @@
|
||||
@import 'bourbon';
|
||||
|
||||
$green: #bada55;
|
||||
$dark-green: shade($green, 25%);
|
||||
$light-green: tint($green, 25%);
|
||||
$gray: #6b6659;
|
||||
$light-gray: tint($gray, 25%);
|
||||
$tan: #cbcbba;
|
||||
$red: #b3151a;
|
||||
$blue: #dae9e5;
|
||||
$yellow: #f8ca00;
|
||||
$black: #000;
|
||||
$white: #fff;
|
||||
|
||||
$base-font-size: 16px;
|
||||
$base-line-height: 24px;
|
||||
|
||||
@mixin box-shadow {
|
||||
@include prefixer(box-shadow, 0 0 10px $black, webkit moz spec);
|
||||
}
|
||||
|
||||
@mixin input-placeholder {
|
||||
&:-moz-placeholder {
|
||||
@content;
|
||||
}
|
||||
|
||||
&::-moz-placeholder {
|
||||
@content;
|
||||
}
|
||||
|
||||
&:-ms-input-placeholder {
|
||||
@content;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@function rhythm($scale, $font-size: $base-font-size) {
|
||||
@return ($scale * $base-line-height / $font-size) * 1em;
|
||||
}
|
||||
|
||||
@mixin adjust-font-size-to($to-size, $lines: auto) {
|
||||
font-size: ($to-size / $base-font-size) * 1em;
|
||||
@if $lines == auto {
|
||||
$lines: ceil($to-size / $base-font-size);
|
||||
}
|
||||
line-height: rhythm($lines, $to-size);
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
@import 'base';
|
||||
@import 'reset';
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: $base-font-size;
|
||||
line-height: $base-line-height;
|
||||
background: $gray;
|
||||
margin-bottom: rhythm(1);
|
||||
}
|
||||
|
||||
a {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.inline-icon {
|
||||
svg {
|
||||
margin-right: rhythm(1/4);
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include adjust-font-size-to(48px, 2);
|
||||
}
|
||||
|
||||
ul.inline-list {
|
||||
@include adjust-font-size-to(14px, 2/3);
|
||||
@include clearfix;
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
||||
&::after {
|
||||
content: '//';
|
||||
padding: 0 rhythm(1/4);
|
||||
}
|
||||
|
||||
&:last-child::after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
min-width: 200px;
|
||||
|
||||
&.loading .svg {
|
||||
position: absolute;
|
||||
top: -10000px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
background: $green;
|
||||
background: linear-gradient(to bottom, $green 0%, $dark-green 100%);
|
||||
padding: rhythm(1);
|
||||
@include box-shadow;
|
||||
@include clearfix;
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
|
||||
span {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Bangers', 'cursive';
|
||||
}
|
||||
|
||||
nav {
|
||||
@include adjust-font-size-to(18px, 1);
|
||||
display: inline-block;
|
||||
margin-left: rhythm(1/4);
|
||||
padding-left: rhythm(1/4);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
|
||||
&:active, &:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: rhythm(1);
|
||||
display: block;
|
||||
|
||||
.copy {
|
||||
background-color: $tan;
|
||||
padding: rhythm(1/2);
|
||||
}
|
||||
|
||||
.changelog {
|
||||
dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dd {
|
||||
&::before {
|
||||
content: '\00BB';
|
||||
font-weight: bold;
|
||||
margin-right: rhythm(1/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
@include adjust-font-size-to($base-font-size * 2);
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background-color: $green;
|
||||
position: relative;
|
||||
padding: rhythm(1);
|
||||
display: inline-block;
|
||||
font-style: italic;
|
||||
float: right;
|
||||
|
||||
&::before {
|
||||
@include adjust-font-size-to($base-font-size * 4);
|
||||
content: '\201c';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include adjust-font-size-to($base-font-size * 4);
|
||||
content: '\201d';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -0.5em;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
|
||||
.documentation {
|
||||
h1 {
|
||||
@include adjust-font-size-to($base-font-size * 2);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include adjust-font-size-to($base-font-size);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include adjust-font-size-to($base-font-size);
|
||||
|
||||
&::before {
|
||||
content: '\00BB';
|
||||
font-weight: bold;
|
||||
margin-right: rhythm(1/4);
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
margin-bottom: rhythm(1);
|
||||
}
|
||||
|
||||
section, div.section {
|
||||
margin: rhythm(1) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: rhythm(1) 0;
|
||||
}
|
||||
|
||||
figure {
|
||||
line-height: 0;
|
||||
background: $white;
|
||||
margin: rhythm(1/4);
|
||||
@include box-shadow;
|
||||
|
||||
&.shift-right {
|
||||
float: right;
|
||||
margin-left: rhythm(1/2);
|
||||
}
|
||||
|
||||
&.shift-left {
|
||||
float: left;
|
||||
margin-right: rhythm(1/2);
|
||||
}
|
||||
|
||||
.svg {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
@include adjust-font-size-to($base-font-size);
|
||||
background: $green;
|
||||
font-weight: bold;
|
||||
padding: 0 rhythm(1/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application {
|
||||
position: relative;
|
||||
@include clearfix;
|
||||
|
||||
form {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include adjust-font-size-to($base-font-size);
|
||||
border: 0 none;
|
||||
outline: none;
|
||||
background: $tan;
|
||||
padding: 0 0.5em;
|
||||
margin-bottom: 0.25em;
|
||||
width: 100% !important; // "!important" prevents user changing width
|
||||
box-sizing: border-box;
|
||||
font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
||||
|
||||
@include input-placeholder {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include adjust-font-size-to($base-font-size);
|
||||
width: 100px;
|
||||
border: 0 none;
|
||||
background: $green;
|
||||
background: linear-gradient(to bottom, $green 0%, $dark-green 100%);
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
float: right;
|
||||
display: none;
|
||||
|
||||
body.has-results & {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.hide-download-png.hide-permalink .download-svg:after,
|
||||
&.hide-permalink .download-png:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hide-permalink .permalink,
|
||||
&.hide-download-svg .download-svg,
|
||||
&.hide-download-png .download-png {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
margin-top: rhythm(1);
|
||||
display: none;
|
||||
|
||||
body.has-results &, body.has-error &, body.is-loading & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 50%;
|
||||
height: rhythm(1/2);
|
||||
border: 1px solid $dark-green;
|
||||
overflow: hidden;
|
||||
margin: rhythm(1) auto;
|
||||
|
||||
div {
|
||||
background: $green;
|
||||
background: linear-gradient(135deg, $green 25%, $light-green 25%, $light-green 50%, $green 50%, $green 75%, $light-green 75%, $light-green 100%);
|
||||
background-size: rhythm(2) rhythm(2);
|
||||
background-repeat: repeat-x;
|
||||
height: 100%;
|
||||
animation: progress 1s infinite linear
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% { background-position-x: rhythm(2); }
|
||||
100% { background-position-x: 0; }
|
||||
}
|
||||
|
||||
#error {
|
||||
background: $red;
|
||||
color: $white;
|
||||
padding: 0 0.5em;
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
display: none;
|
||||
overflow-x: auto;
|
||||
|
||||
body.has-error & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#warnings {
|
||||
@include adjust-font-size-to($base-font-size, 1);
|
||||
font-weight: bold;
|
||||
background-color: $yellow;
|
||||
display: none;
|
||||
|
||||
li {
|
||||
margin: rhythm(1/4);
|
||||
}
|
||||
|
||||
body.has-results & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#regexp-render {
|
||||
background: $white;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
display: none;
|
||||
|
||||
body.is-loading &,
|
||||
body.has-results & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#open-iconic {
|
||||
display: none;
|
||||
|
||||
path {
|
||||
stroke: none;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 0 rhythm(1);
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
width: 80px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
@import 'base';
|
||||
|
||||
svg {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.root text,
|
||||
.root tspan {
|
||||
font: 12px Arial;
|
||||
}
|
||||
|
||||
.root path {
|
||||
fill-opacity: 0;
|
||||
stroke-width: 2px;
|
||||
stroke: $black;
|
||||
}
|
||||
|
||||
.root circle {
|
||||
fill: $gray;
|
||||
stroke-width: 2px;
|
||||
stroke: $black;
|
||||
}
|
||||
|
||||
.anchor text, .any-character text {
|
||||
fill: $white;
|
||||
}
|
||||
|
||||
.anchor rect, .any-character rect {
|
||||
fill: $gray;
|
||||
}
|
||||
|
||||
.escape text, .charset-escape text, .literal text {
|
||||
fill: $black;
|
||||
}
|
||||
|
||||
.escape rect, .charset-escape rect {
|
||||
fill: $green;
|
||||
}
|
||||
|
||||
.literal rect {
|
||||
fill: $blue;
|
||||
}
|
||||
|
||||
.charset .charset-box {
|
||||
fill: $tan;
|
||||
}
|
||||
|
||||
.subexp .subexp-label tspan,
|
||||
.charset .charset-label tspan,
|
||||
.match-fragment .repeat-label tspan {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.repeat-label {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.subexp .subexp-label tspan,
|
||||
.charset .charset-label tspan {
|
||||
dominant-baseline: text-after-edge;
|
||||
}
|
||||
|
||||
.subexp .subexp-box {
|
||||
stroke: $light-gray;
|
||||
stroke-dasharray: 6,2;
|
||||
stroke-width: 2px;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.quote {
|
||||
fill: $light-gray;
|
||||
}
|
||||
Reference in New Issue
Block a user