frontend and backend

This commit is contained in:
2022-01-09 12:08:42 +01:00
parent cbff7c5d22
commit dde65761e5
75 changed files with 37830 additions and 19 deletions

178
frontend/src/App.js Normal file
View File

@ -0,0 +1,178 @@
import React, { Component } from 'react';
import axios from 'axios';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Header from "./Header";
import Tabs from "./preg/Tabs";
import Opts from "./preg/Opts";
import Fields from "./preg/Fields";
import Return from "./preg/Return";
import Code from "./preg/Code";
import Output from "./preg/Output";
import Footer from "./Footer";
import Help from "./preg/Help";
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
method: 'preg_match',
pattern: '',
replacement: '',
subject: '',
offset: null,
limit: null,
delimiter: null,
PREG_OFFSET_CAPTURE: false,
PREG_UNMATCHED_AS_NULL: false,
PREG_SET_ORDER: false,
PREG_SPLIT_NO_EMPTY: false,
PREG_SPLIT_DELIM_CAPTURE: false,
PREG_SPLIT_OFFSET_CAPTURE: false,
response: {},
help: false
}
this.timer = null
this.apiUrl = window.location.href +'api/preg'
//this.apiUrl = 'http://localhost:8086/api/preg'
this.maps = {
'preg_match': {
'fields': ['pattern', 'subject'],
'opts': ['PREG_OFFSET_CAPTURE', 'PREG_UNMATCHED_AS_NULL', 'offset']
},
'preg_match_all': {
'fields': ['pattern', 'subject'],
'opts': ['PREG_PATTERN_ORDER', 'PREG_SET_ORDER', 'PREG_OFFSET_CAPTURE', 'PREG_UNMATCHED_AS_NULL', 'offset']
},
'preg_split': {
'fields': ['pattern', 'subject'],
'opts': ['PREG_SPLIT_NO_EMPTY', 'PREG_SPLIT_DELIM_CAPTURE', 'PREG_SPLIT_OFFSET_CAPTURE', 'limit']
},
'preg_replace': {
'fields': ['pattern', 'subject', 'replacement'],
'opts': ['limit']
},
'preg_quote': {
'fields': ['pattern'],
'opts': ['delimeter']
}
}
}
componentDidMount() {
window.addEventListener("hashchange", this.hashChange, false)
this.hashChange()
}
hashChange = () => {
const hash = window.location.hash.replace(/^#/, '')
if (hash && hash !== 'close' && hash !== 'help' && hash !== this.state.response.hash) {
axios.get(this.apiUrl, { params: { hash } }).then((res) => {
this.setState({ ...res.data, response: {} }, () => {
this.api()
})
})
}
}
methodSelect = (method) => {
this.setState({method}, this.api)
}
handleInputChange = (event) => {
const target = event.target
const value = (target.type === 'checkbox')?
target.checked:
(target.type === 'number'?
parseInt(target.value):
target.value)
const name = target.name
this.setState({ [name]: value }, this.delay)
}
api = () => {
const { response, help, ...state } = this.state
axios.post(this.apiUrl, state).then((res) => {
this.setState({ response: res.data }, () => {
if ('hash' in res.data) {
window.location.hash = '#'+res.data.hash;
}
})
}).catch((error) => {
//console.log(error.response)
this.setState({ response: {
fatal: (error.response? error.response.data.fatal: error.message)
} }, () => {
window.history.replaceState(null, null, ' ');
})
});
}
delay = () => {
clearTimeout(this.timer)
this.timer = setTimeout(this.api, 200)
}
showHelp = (show) => {
this.setState({ help: show })
}
render() {
const { method, pattern, replacement, subject, response, help, ...state } = this.state
const maps = this.maps[method]
return (
<>
<Header method={method} hash={response.hash? window.location.origin+"/#"+response.hash: null} onClick={() => this.showHelp(true)} />
<Container>
<Row className="mt-3 mb-3">
<Col>
<Tabs method={method} functions={Object.keys(this.maps)} onChange={this.methodSelect} />
</Col>
</Row>
<Fields method={method} fields={maps.fields} pattern={pattern} replacement={replacement} subject={subject} onChange={this.handleInputChange} />
<div className="separator">options</div>
<Opts method={method} opts={maps.opts} {...state} onChange={this.handleInputChange} />
<Row className="mt-3">
<Col xs="3" sm="2">
<div className="separator">return</div>
<Return response={response} />
</Col>
<Col>
<div className="separator">snippet</div>
<Code response={response} />
</Col>
</Row>
<Row className="mt-3">
<Col>
<div className="separator">$matches or result</div>
<Output response={response} />
</Col>
</Row>
<Row className="mt-3 text-end">
<Col>
<Footer />
</Col>
</Row>
</Container>
<Help show={help} onHide={() => this.showHelp(false)} />
</>
);
}
}

16
frontend/src/Footer.js Normal file
View File

@ -0,0 +1,16 @@
import React, { Component } from 'react';
import { BsFileEarmarkCodeFill, BsTwitter } from "react-icons/bs";
export default class Footer extends Component {
render() {
return (
<footer>
<small>
Coded by <a href="https://twitter.com/xergio">Sergio Álvarez <BsTwitter /></a>{' '}
<span className="text-muted">¦</span>{' '}
<a href="https://sergio.am/code/xrg.es"><BsFileEarmarkCodeFill /></a></small>
</footer>
)
}
}

53
frontend/src/Header.js Normal file
View File

@ -0,0 +1,53 @@
import React, { Component } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { BsFillQuestionCircleFill, BsFillInfoCircleFill } from "react-icons/bs";
import { FiPaperclip } from "react-icons/fi";
import Container from 'react-bootstrap/Container';
import Navbar from 'react-bootstrap/Navbar';
import Nav from 'react-bootstrap/Nav';
export default class Header extends Component {
state = {
copied: false,
};
render() {
return (
<Navbar bg="light" expand="sm">
<Container>
<Navbar.Brand href="/">
<img
alt=""
src="/img/safari-pinned-tab.svg"
width="30"
height="30"
className="d-inline-block align-top"
/>{' '}
<strong>XRG</strong>
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<Nav.Link href="/">RegExp</Nav.Link>
<Nav.Link href="/dencode">D/Encode</Nav.Link>
</Nav>
<Nav>
{this.props.hash &&
<Nav.Link href={this.props.hash} title="Permalink" className="text-success">
<CopyToClipboard text={this.props.hash} onCopy={() => this.setState({copied: true})}>
<FiPaperclip />
</CopyToClipboard>
</Nav.Link>
}
<Nav.Link href={'https://php.net/'+this.props.method} title="PHP.net documentation"><BsFillInfoCircleFill /></Nav.Link>
<Nav.Link href="#help" title="Help" onClick={this.props.onClick}><BsFillQuestionCircleFill /></Nav.Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
)
}
}

View File

@ -0,0 +1,35 @@
// Cosmo 5.1.3
// Bootswatch
// Variables
$web-font-path: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap" !default;
@if $web-font-path {
@import url($web-font-path);
}
// Typography
body {
-webkit-font-smoothing: antialiased;
}
// Indicators
.badge {
&.bg-light {
color: $dark;
}
}
// Progress bars
.progress {
@include box-shadow(none);
.progress-bar {
font-size: 8px;
line-height: 8px;
}
}

View File

@ -0,0 +1,69 @@
// Cosmo 5.1.3
// Bootswatch
$theme: "cosmo" !default;
//
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #495057 !default;
$gray-800: #373a3c !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #2780e3 !default;
$indigo: #6610f2 !default;
$purple: #613d7c !default;
$pink: #e83e8c !default;
$red: #ff0039 !default;
$orange: #f0ad4e !default;
$yellow: #ff7518 !default;
$green: #3fb618 !default;
$teal: #20c997 !default;
$cyan: #9954bb !default;
$primary: $blue !default;
$secondary: $gray-800 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-800 !default;
$min-contrast-ratio: 2.6 !default;
// Options
$enable-rounded: false !default;
// Body
$body-color: $gray-800 !default;
// Fonts
// stylelint-disable-next-line value-keyword-case
$font-family-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$headings-font-weight: 400 !default;
// Navbar
$navbar-dark-hover-color: rgba($white, 1) !default;
$navbar-light-hover-color: rgba($black, .9) !default;
// Alerts
$alert-border-width: 0 !default;
// Progress bars
$progress-height: .5rem !default;

11
frontend/src/index.js Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.scss';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

43
frontend/src/index.scss Normal file
View File

@ -0,0 +1,43 @@
.separator {
display: flex;
color: #ddd;
font-size: .9rem;
margin-top: 1.5rem;
}
/*.separator::after {
content: '';
flex: 1;
height: 0.08rem;
background: linear-gradient(90deg, rgba(231,233,237,1) 0%, rgba(255,255,255,1) 80%);
transform: translateY(.75rem);
}
.separator:not(:empty)::after {
margin-left: .25em;
}*/
//$body-bg: #567890;
.table {
width: unset !important;
}
table td:first-child:before {
content: '[';
color: var(--bs-table-bg);
}
table td:first-child:after {
content: ']';
color: var(--bs-table-bg);
}
.nav-item:hover {
background-color: #eaeaea !important;
}
@import "_variables";
@import "~bootstrap/scss/bootstrap";
@import "_bootswatch";

19
frontend/src/preg/Code.js Normal file
View File

@ -0,0 +1,19 @@
import React, { Component } from 'react';
function createMarkup(html) {
return {__html: html}
}
export default class Code extends Component {
render() {
const { response } = this.props;
return (
<>
{'code' in response &&
<code dangerouslySetInnerHTML={createMarkup(response.code)} />
}
</>
)
}
}

View File

@ -0,0 +1,29 @@
import React, { Component } from 'react';
import Form from 'react-bootstrap/Form';
export default class Fields extends Component {
render() {
return (
<>
{this.props.fields.includes('pattern') &&
<Form.Group className="mb-3" controlId="pattern">
<Form.Label>Regular Expression / Pattern</Form.Label>
<Form.Control type="text" onChange={this.props.onChange} name="pattern" value={this.props.pattern} placeholder="#...#" className="font-monospace" />
</Form.Group>
}
{this.props.fields.includes('replacement') &&
<Form.Group className="mb-3" controlId="replacement">
<Form.Label>Replacement</Form.Label>
<Form.Control type="text" onChange={this.props.onChange} name="replacement" value={this.props.replacement} />
</Form.Group>
}
{this.props.fields.includes('subject') &&
<Form.Group className="mb-3" controlId="subject">
<Form.Label>String / Subject</Form.Label>
<Form.Control as="textarea" onChange={this.props.onChange} name="subject" value={this.props.subject} rows={3} />
</Form.Group>
}
</>
);
}
}

102
frontend/src/preg/Help.js Normal file
View File

@ -0,0 +1,102 @@
import React, { Component } from 'react';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Modal from 'react-bootstrap/Modal';
export default class Help extends Component {
render() {
return (
<Modal {...this.props} size="xl">
<Modal.Header closeButton>
<Modal.Title>Regexp spreadsheet (<a href="http://php.net/manual/en/pcre.pattern.php">full documentation</a>)</Modal.Title>
</Modal.Header>
<Modal.Body>
<Container>
<Row>
<Col>
<h4>Pattern Modifiers</h4>
<div><code>i</code> PCRE_CASELESS</div>
<div><code>m</code> PCRE_MULTILINE</div>
<div><code>s</code> PCRE_DOTALL</div>
<div><code>x</code> PCRE_EXTENDED</div>
<div><code>A</code> PCRE_ANCHORED</div>
<div><code>D</code> PCRE_DOLLAR_ENDONLY</div>
<div><code>S</code> Extra analysis</div>
<div><code>U</code> PCRE_UNGREEDY</div>
<div><code>X</code> PCRE_EXTRA</div>
<div><code>J</code> PCRE_INFO_JCHANGED</div>
<div><code>u</code> PCRE_UTF8</div>
</Col>
<Col>
<h4>Meta-characters outside <code>[ ]</code></h4>
<div><code>\</code> general escape character</div>
<div><code>^</code> assert start of subject (or line, in multiline mode)</div>
<div><code>$</code> assert end of subject (or line, in multiline mode)</div>
<div><code>.</code> match any character except newline (by default)</div>
<div><code>[ ]</code> character class definition</div>
<div><code>|</code> start of alternative branch</div>
<div><code>( )</code> subpattern</div>
<div><code>?</code> extends the meaning of '(', also 0 or 1 quantifier</div>
<div><code>*</code> 0 or more quantifier</div>
<div><code>+</code> 1 or more quantifier</div>
<div><code>{'{ }'}</code> min/max quantifier, {'{n[,n]}'}</div>
<h4 className="mt-4">Meta-characters inside <code>[ ]</code></h4>
<div><code>\</code> general escape character</div>
<div><code>^</code> negate the class, but only if the first character</div>
<div><code>-</code> indicates character range</div>
<h4 className="mt-4">Others</h4>
<div><code>{'\\1-9'}</code> in-group back references</div>
<div><code>(?P&lt;lbl&gt;...)</code> labelize subpatterns</div>
<div><code>(?:...)</code> non-capture group</div>
<div><code>(?&gt;...)</code> Atomic group</div>
<div><code>(?=...)</code> Positive lookahead</div>
<div><code>(?!...)</code> Negative lookahead</div>
<div><code>(?&lt;=..)</code> Positive lookbehind</div>
<div><code>(?&lt;!..)</code> Negative lookbehind</div>
<div><code>(?(?=.).|.)</code> if . then . else .</div>
<div><code>(?#...)</code> Comment</div>
</Col>
<Col>
<h4>Scape sequences</h4>
<div><code>\a</code> alarm, that is, the BEL character (hex 07)</div>
<div><code>\cx</code> "control-x", where x is any character</div>
<div><code>\e</code> escape (hex 1B)</div>
<div><code>\f</code> formfeed (hex 0C)</div>
<div><code>\n</code> newline (hex 0A)</div>
<div><code>\r</code> carriage return (hex 0D)</div>
<div><code>\R</code> line break: matches \n, \r and \r\n</div>
<div><code>\t</code> tab (hex 09)</div>
<div><code>\p{'{xx}'}</code> a character with the xx <a href="http://www.php.net/manual/en/regexp.reference.unicode.php">property</a></div>
<div><code>\P{'{xx}'}</code> a character without the xx <a href="http://www.php.net/manual/en/regexp.reference.unicode.php">property</a></div>
<div><code>\xhh</code> character with hex code hh</div>
<div><code>\ddd</code> character with octal code ddd, or backreference</div>
<div><code>\d</code> any decimal digit</div>
<div><code>\D</code> any character that is not a decimal digit</div>
<div><code>\s</code> any whitespace character</div>
<div><code>\S</code> any character that is not a whitespace character</div>
<div><code>\h</code> any horizontal whitespace character</div>
<div><code>\H</code> any character that is not a horizontal whitespace</div>
<div><code>\v</code> any vertical whitespace character</div>
<div><code>\V</code> any character that is not a vertical whitespace character</div>
<div><code>\w</code> any "word" character</div>
<div><code>\W</code> any "non-word" character</div>
<div><code>\b</code> word boundary</div>
<div><code>\B</code> not a word boundary</div>
<div><code>\A</code> start of subject (independent of multiline mode)</div>
<div><code>\Z</code> end of subject or newline at end (independent of multiline mode)</div>
<div><code>\z</code> end of subject (independent of multiline mode)</div>
<div><code>\G</code> first matching position in subject</div>
</Col>
</Row>
</Container>
</Modal.Body>
</Modal>
)
}
}

55
frontend/src/preg/Opts.js Normal file
View File

@ -0,0 +1,55 @@
import React, { Component } from 'react';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import Col from 'react-bootstrap/Col';
function Checkbox(props) {
return props.opts.includes(props.id)? (
<Form.Group className="mb-3" controlId={props.id}>
<Form.Check type="checkbox" name={props.id} label={props.id} checked={props[props.id]} onChange={props.onChange} />
</Form.Group>
): '';
}
export default class Opts extends Component {
render() {
return (
<>
{this.props.opts.includes('PREG_SET_ORDER') &&
<p className="text-muted"><tt>PREG_PATTERN_ORDER</tt> is the default order method.</p>
}
<Checkbox id="PREG_SET_ORDER" {...this.props} />
<Checkbox id="PREG_OFFSET_CAPTURE" {...this.props} />
<Checkbox id="PREG_UNMATCHED_AS_NULL" {...this.props} />
<Checkbox id="PREG_SPLIT_NO_EMPTY" {...this.props} />
<Checkbox id="PREG_SPLIT_DELIM_CAPTURE" {...this.props} />
<Checkbox id="PREG_SPLIT_OFFSET_CAPTURE" {...this.props} />
{this.props.opts.includes('offset') &&
<Col xl="2" md="3" sm="4" xs="6">
<InputGroup className="mb-3">
<InputGroup.Text id="offset">offset</InputGroup.Text>
<Form.Control type="number" name="offset" placeholder="0" aria-label="offset" aria-describedby="offset" value={this.props.offset ?? ''} onChange={this.props.onChange} min="0" />
</InputGroup>
</Col>
}
{this.props.opts.includes('limit') &&
<Col xl="2" md="3" sm="4" xs="6">
<InputGroup className="mb-3">
<InputGroup.Text id="limit">limit</InputGroup.Text>
<Form.Control type="number" name="limit" placeholder="-1" aria-label="limit" aria-describedby="limit" value={this.props.limit ?? ''} onChange={this.props.onChange} min="-1" />
</InputGroup>
</Col>
}
{this.props.opts.includes('delimeter') &&
<Col xl="2" md="3" sm="4" xs="6">
<InputGroup className="mb-3">
<InputGroup.Text id="delimiter">delimiter</InputGroup.Text>
<Form.Control name="delimiter" aria-label="delimiter" aria-describedby="delimiter" value={this.props.delimiter ?? ''} onChange={this.props.onChange} />
</InputGroup>
</Col>
}
</>
);
}
}

View File

@ -0,0 +1,70 @@
import React, { Component } from 'react';
import Table from 'react-bootstrap/Table'
function mapObject(object, callback) {
return Object.keys(object).map(key => {
return callback(key, object[key])
})
}
const DumpList = (props) => {
const type = typeof props.dump
if (type === "object") {
if (Object.keys(props.dump).length === 0) {
return <tt className="text-secondary">[]</tt>
}
const items = mapObject(props.dump, (key, value) => {
if (['number', 'string'].includes(typeof value)) {
// .replace(" ", '⎵')
return (
<tr key={key}>
<td className="table-active text-end text-muted font-monospace">{key}</td>
<td className="px-2">{value}</td>
</tr>
)
} else {
return (
<tr key={key}>
<td className="table-active text-end text-muted font-monospace">{key}</td>
<td><DumpList dump={value} indent={props.indent + 1} /></td>
</tr>
)
}
})
return (
<Table borderless variant="light" size="sm">
<tbody>
{items}
</tbody>
</Table>
)
} else if (type === 'string') {
return (<tt>{props.dump}</tt>)
}
return (<em>empty result</em>)
}
export default class Output extends Component {
render() {
const { response } = this.props;
return (
<>
{'fatal' in response &&
<div>
<code>{response.fatal}</code>
</div>
}
{'dump' in response &&
<DumpList dump={response.dump} indent={0} />
}
</>
)
}
}

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
export default class Return extends Component {
render() {
const { response } = this.props;
return (
<>
{'returnType' in response && 'returnValue' in response &&
<span>
<code>{response.returnType}</code>:{' '}<code>{response.returnValue}</code>
</span>
}
</>
)
}
}

20
frontend/src/preg/Tabs.js Normal file
View File

@ -0,0 +1,20 @@
import React, { Component } from 'react';
import Nav from 'react-bootstrap/Nav';
export default class Tabs extends Component {
render() {
const tabs = this.props.functions.map((func) => {
return (
<Nav.Item key={func} className="bg-light font-monospace"><Nav.Link eventKey={func}>{func}</Nav.Link></Nav.Item>
)
})
return (
<Nav fill variant="pills" defaultActiveKey={this.props.functions[0]} activeKey={this.props.method} onSelect={this.props.onChange}>
{tabs}
</Nav>
);
}
}