mirror of
https://github.com/laurent22/joplin.git
synced 2025-02-01 19:15:01 +02:00
Mobile: Removing no longer used beta Markdown editor
Ref: https://discourse.joplinapp.org/t/anyone-using-the-beta-editor-on-ios/11658
This commit is contained in:
parent
86610e7561
commit
0e2351e79e
@ -1,23 +0,0 @@
|
||||
import applyWrapFormat from './applyWrapFormat';
|
||||
import applyWrapFormatNewLines from './applyWrapFormatNewLines';
|
||||
import applyListFormat from './applyListFormat';
|
||||
import applyWebLinkFormat from './applyWebLinkFormat';
|
||||
|
||||
export default [
|
||||
{ key: 'B', title: 'B', wrapper: '**', onPress: applyWrapFormat, style: { fontWeight: 'bold' } },
|
||||
{ key: 'I', title: 'I', wrapper: '*', onPress: applyWrapFormat, style: { fontStyle: 'italic' } },
|
||||
{ key: 'Link', title: 'Link', onPress: applyWebLinkFormat },
|
||||
{ key: 'List', title: 'List', prefix: '-', onPress: applyListFormat },
|
||||
{
|
||||
key: 'S',
|
||||
title: 'S',
|
||||
wrapper: '~~',
|
||||
onPress: applyWrapFormat,
|
||||
style: { textDecorationLine: 'line-through' },
|
||||
},
|
||||
{ key: '</>', title: '</>', wrapper: '`', onPress: applyWrapFormat },
|
||||
{ key: 'Pre', title: 'Pre', wrapper: '```', onPress: applyWrapFormatNewLines },
|
||||
{ key: 'H1', title: 'H1', prefix: '#', onPress: applyListFormat },
|
||||
{ key: 'H2', title: 'H2', prefix: '##', onPress: applyListFormat },
|
||||
{ key: 'H3', title: 'H3', prefix: '###', onPress: applyListFormat },
|
||||
];
|
@ -1,162 +0,0 @@
|
||||
/**
|
||||
* Inspired by https://github.com/kunall17/MarkdownEditor
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
Platform,
|
||||
KeyboardAvoidingView,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { renderFormatButtons } from './renderButtons';
|
||||
import NoteBodyViewer from '@joplin/lib/components/NoteBodyViewer/NoteBodyViewer';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
screen: { // Wrapper around the editor and the preview
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
});
|
||||
|
||||
const MarkdownPreviewButton = (props) =>
|
||||
<TouchableOpacity
|
||||
onPress={props.convertMarkdown}
|
||||
style={{ padding: 8, borderRightWidth: 1, borderColor: props.borderColor }}>
|
||||
<Image
|
||||
style={{ tintColor: props.color, padding: 8 }}
|
||||
source={require('./static/visibility.png')}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>;
|
||||
|
||||
export default class MarkdownEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
text: props.value,
|
||||
selection: { start: 0, end: 0 },
|
||||
// Show preview by default
|
||||
showPreview: props.showPreview ? props.showPreview : true,
|
||||
};
|
||||
this.textAreaRef = React.createRef(); // For focusing the textarea
|
||||
}
|
||||
textInput: TextInput;
|
||||
|
||||
changeText = (selection: {start: number, end: number}) => (input: string) => {
|
||||
let result = input;
|
||||
const cursor = selection.start;
|
||||
const isOnNewline = '\n' === input.slice(cursor - 1, cursor);
|
||||
const isDeletion = input.length < this.state.text.length;
|
||||
if (isOnNewline && !isDeletion) {
|
||||
const prevLines = input.slice(0, cursor - 1).split('\n');
|
||||
const prevLine = prevLines[prevLines.length - 1];
|
||||
|
||||
const insertListLine = (bullet) => ([
|
||||
prevLines.join('\n'), // Previous text
|
||||
`\n${bullet} `, // Current line with new bullet point
|
||||
input.slice(cursor, input.length), // Following text
|
||||
].join(''));
|
||||
|
||||
const insertedEndListLine = [
|
||||
// Previous text (all but last bullet line, which we remove)
|
||||
prevLines.slice(0, prevLines.length - 1).join('\n') ,
|
||||
'\n\n', // Two newlines to get out of the list
|
||||
input.slice(cursor, input.length), // Following text
|
||||
].join('');
|
||||
|
||||
// Add new ordered list line item
|
||||
if (prevLine.startsWith('- ') && !prevLine.startsWith('- [ ')) {
|
||||
// If the bullet on the previous line isn't empty, add a new bullet.
|
||||
if (prevLine.trim() !== '-') {
|
||||
result = insertListLine('-');
|
||||
} else {
|
||||
result = insertedEndListLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new checklist line item
|
||||
if ((prevLine.startsWith('- [ ] ') || prevLine.startsWith('- [x] '))) {
|
||||
// If the bullet on the previous line isn't empty, add a new bullet.
|
||||
if (prevLine.trim() !== '- [ ]' && prevLine.trim() !== '- [x]') {
|
||||
result = insertListLine('- [ ]');
|
||||
} else {
|
||||
result = insertedEndListLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new ordered list item
|
||||
if (/^\d+\./.test(prevLine)) {
|
||||
// If the bullet on the previous line isn't empty, add a new bullet.
|
||||
const digit = Number(prevLine.match(/^\d+/)[0]);
|
||||
if (prevLine.trim() !== `${digit}.`) {
|
||||
result = insertListLine(`${digit + 1}.`);
|
||||
} else {
|
||||
result = insertedEndListLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hide Markdown preview on text change
|
||||
this.setState({ text: result, showPreview: false });
|
||||
this.props.saveText(result);
|
||||
if (this.props.onMarkdownChange) this.props.onMarkdownChange(input);
|
||||
};
|
||||
|
||||
onSelectionChange = event => {
|
||||
this.setState({ selection: event.nativeEvent.selection });
|
||||
};
|
||||
|
||||
focus = () => this.textAreaRef.current.focus()
|
||||
|
||||
convertMarkdown = () => this.setState({ showPreview: !this.state.showPreview })
|
||||
|
||||
render() {
|
||||
const WrapperView = Platform.OS === 'ios' ? KeyboardAvoidingView : View;
|
||||
const { Formats, markdownButton } = this.props;
|
||||
const { text, selection, showPreview } = this.state;
|
||||
return (
|
||||
<WrapperView style={styles.screen}>
|
||||
<TextInput
|
||||
{...this.props}
|
||||
multiline
|
||||
autoCapitalize="sentences"
|
||||
underlineColorAndroid="transparent"
|
||||
onChangeText={this.changeText(selection)}
|
||||
onSelectionChange={this.onSelectionChange}
|
||||
value={text}
|
||||
ref={this.textAreaRef}
|
||||
selection={selection}
|
||||
/>
|
||||
{showPreview && <NoteBodyViewer {...this.props.noteBodyViewer} />}
|
||||
<View style={styles.buttonContainer}>
|
||||
<MarkdownPreviewButton
|
||||
convertMarkdown={this.convertMarkdown}
|
||||
borderColor={this.props.borderColor}
|
||||
color={this.props.markdownButtonsColor}
|
||||
/>
|
||||
{renderFormatButtons(
|
||||
{
|
||||
color: this.props.markdownButtonsColor,
|
||||
getState: () => this.state,
|
||||
setState: (state, callback) => {
|
||||
// Hide Markdown preview on text change
|
||||
this.setState({ showPreview: false });
|
||||
this.setState(state, callback);
|
||||
},
|
||||
},
|
||||
Formats,
|
||||
markdownButton
|
||||
)}
|
||||
</View>
|
||||
</WrapperView>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { replaceBetween } from './utils';
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
||||
export default ({ getState, item, setState }) => {
|
||||
let { text } = getState();
|
||||
const { selection } = getState();
|
||||
text = text || '';
|
||||
let newText;
|
||||
let newSelection;
|
||||
|
||||
// Ignore multi-character selections.
|
||||
// NOTE: I was on the fence about whether more appropriate behavior would be
|
||||
// to add the list prefix (e.g. '-', '1.', '#', '##', '###') at the
|
||||
// beginning of the line where the selection begins, but for now I think
|
||||
// it's more natural to just ignore it in this case. If after using this
|
||||
// editor for a while it turns out the other way is more natural, that's
|
||||
// fine by me!
|
||||
if (selection.start !== selection.end) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spaceForPrefix = item.prefix.length + 1;
|
||||
const isNewLine = text.substring(selection.start - 1, selection.start) === '\n';
|
||||
if (isNewLine) { // We're at the start of a line
|
||||
newText = replaceBetween(text, selection, `${item.prefix} `);
|
||||
newSelection = { start: selection.start + spaceForPrefix, end: selection.start + spaceForPrefix };
|
||||
} else { // We're in the middle of a line
|
||||
// NOTE: It may be more natural for the prefix (e.g. '-', '1.', '#', '##')
|
||||
// to be prepended at the beginning of the line where the selection is,
|
||||
// rather than creating a new line (which is the behavior implemented here).
|
||||
// If the other way is more natural, that's fine by me!
|
||||
newText = replaceBetween(text, selection, `\n${item.prefix} `);
|
||||
newSelection = {
|
||||
start: selection.start + spaceForPrefix + 1,
|
||||
end: selection.start + spaceForPrefix + 1,
|
||||
};
|
||||
}
|
||||
|
||||
setState({ text: newText }, () => {
|
||||
shim.setTimeout(() => {
|
||||
setState({ selection: newSelection });
|
||||
}, 300);
|
||||
});
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
import { isStringWebLink, replaceBetween } from './utils';
|
||||
|
||||
export const writeUrlTextHere = 'https://example.com';
|
||||
export const writeTextHereString = 'Write some text here';
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
export default ({ getState, item, setState }) => {
|
||||
const { selection, text } = getState();
|
||||
let newText;
|
||||
let newSelection;
|
||||
const selectedText = text.substring(selection.start, selection.end);
|
||||
if (selection.start !== selection.end) {
|
||||
if (isStringWebLink(selectedText)) {
|
||||
newText = replaceBetween(text, selection, `[${writeTextHereString}](${selectedText})`);
|
||||
newSelection = {
|
||||
start: selection.start + 1,
|
||||
end: selection.start + 1 + writeTextHereString.length,
|
||||
};
|
||||
} else {
|
||||
newText = replaceBetween(text, selection, `[${selectedText}](${writeUrlTextHere})`);
|
||||
newSelection = {
|
||||
start: selection.end + 3,
|
||||
end: selection.end + 3 + writeUrlTextHere.length,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
newText = replaceBetween(text, selection, `[${writeTextHereString}](${writeUrlTextHere})`);
|
||||
newSelection = {
|
||||
start: selection.start + 1,
|
||||
end: selection.start + 1 + writeTextHereString.length,
|
||||
};
|
||||
}
|
||||
setState({ text: newText }, () => {
|
||||
shim.setTimeout(() => {
|
||||
setState({ selection: newSelection });
|
||||
}, 25);
|
||||
});
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
import { replaceBetween } from './utils';
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
||||
export default ({ getState, item, setState }) => {
|
||||
const { text, selection } = getState();
|
||||
const newText = replaceBetween(
|
||||
text,
|
||||
selection,
|
||||
item.wrapper.concat(text.substring(selection.start, selection.end), item.wrapper)
|
||||
);
|
||||
let newPosition;
|
||||
if (selection.start === selection.end) {
|
||||
newPosition = selection.end + item.wrapper.length;
|
||||
} else {
|
||||
newPosition = selection.end + item.wrapper.length * 2;
|
||||
}
|
||||
const extra = {
|
||||
selection: {
|
||||
start: newPosition,
|
||||
end: newPosition,
|
||||
},
|
||||
};
|
||||
setState({ text: newText }, () => {
|
||||
shim.setTimeout(() => {
|
||||
setState({ ...extra });
|
||||
}, 25);
|
||||
});
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
import { replaceBetween } from './utils';
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
||||
export default ({ getState, item, setState }) => {
|
||||
const { text, selection } = getState();
|
||||
let newText = replaceBetween(
|
||||
text,
|
||||
selection,
|
||||
`\n${item.wrapper.concat(
|
||||
'\n',
|
||||
text.substring(selection.start, selection.end),
|
||||
'\n',
|
||||
item.wrapper,
|
||||
'\n'
|
||||
)}`
|
||||
);
|
||||
let newPosition;
|
||||
if (selection.start === selection.end) {
|
||||
newPosition = selection.end + item.wrapper.length + 2; // +2 For two new lines
|
||||
newText = replaceBetween(
|
||||
text,
|
||||
selection,
|
||||
`\n${item.wrapper.concat(
|
||||
'\n',
|
||||
text.substring(selection.start, selection.end),
|
||||
'\n',
|
||||
item.wrapper,
|
||||
'\n'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
newPosition = selection.end + item.wrapper.length * 2 + 3; // +3 For three new lines
|
||||
newText = replaceBetween(
|
||||
text,
|
||||
selection,
|
||||
`${item.wrapper.concat(
|
||||
'\n',
|
||||
text.substring(selection.start, selection.end),
|
||||
'\n',
|
||||
item.wrapper,
|
||||
'\n'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
const extra = {
|
||||
selection: {
|
||||
start: newPosition,
|
||||
end: newPosition,
|
||||
},
|
||||
};
|
||||
setState({ text: newText }, () => {
|
||||
shim.setTimeout(() => {
|
||||
setState({ ...extra });
|
||||
}, 25);
|
||||
});
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import applyWrapFormat from './applyWrapFormat';
|
||||
import applyWrapFormatNewLines from './applyWrapFormatNewLines';
|
||||
import applyListFormat from './applyListFormat';
|
||||
import applyWebLinkFormat from './applyWebLinkFormat';
|
||||
import MarkdownEditor from './MarkdownEditor';
|
||||
|
||||
module.exports = {
|
||||
MarkdownEditor,
|
||||
applyWrapFormat,
|
||||
applyWrapFormatNewLines,
|
||||
applyListFormat,
|
||||
applyWebLinkFormat,
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FlatList, TouchableOpacity, Text } from 'react-native';
|
||||
|
||||
import Formats from './Formats';
|
||||
|
||||
const defaultStyles = { padding: 8, fontSize: 16 };
|
||||
|
||||
const defaultMarkdownButton = ({ item, getState, setState, color }) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={() => item.onPress({ getState, setState, item })}>
|
||||
<Text style={[defaultStyles, item.style, { color: color }]}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const renderFormatButtons = ({ getState, setState, color }, formats, markdownButton) => {
|
||||
const list = (
|
||||
<FlatList
|
||||
data={formats ? formats : Formats}
|
||||
keyboardShouldPersistTaps="always"
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
renderItem={({ item, index }) =>
|
||||
markdownButton
|
||||
? markdownButton({ item, getState, setState })
|
||||
: defaultMarkdownButton({ item, getState, setState, color })}
|
||||
horizontal
|
||||
/>
|
||||
);
|
||||
return list;
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 309 B |
@ -1,9 +0,0 @@
|
||||
import regexValidator from './webLinkValidator';
|
||||
|
||||
export const replaceBetween = (text: string, selection: Object, what: string) =>
|
||||
text.substring(0, selection.start) + what + text.substring(selection.end);
|
||||
|
||||
export const isStringWebLink = (text: string): boolean => {
|
||||
const pattern = regexValidator;
|
||||
return pattern.test(text);
|
||||
};
|
@ -1,104 +0,0 @@
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
|
||||
//
|
||||
// Regular Expression for URL validation
|
||||
//
|
||||
// Author: Diego Perini
|
||||
// Updated: 2010/12/05
|
||||
// License: MIT
|
||||
//
|
||||
// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
// the regular expression composed & commented
|
||||
// could be easily tweaked for RFC compliance,
|
||||
// it was expressly modified to fit & satisfy
|
||||
// these test for an URL shortener:
|
||||
//
|
||||
// http://mathiasbynens.be/demo/url-regex
|
||||
//
|
||||
// Notes on possible differences from a standard/generic validation:
|
||||
//
|
||||
// - utf-8 char class take in consideration the full Unicode range
|
||||
// - TLDs have been made mandatory so single names like "localhost" fails
|
||||
// - protocols have been restricted to ftp, http and https only as requested
|
||||
//
|
||||
// Changes:
|
||||
//
|
||||
// - IP address dotted notation validation, range: 1.0.0.0 - 223.255.255.255
|
||||
// first and last IP address of each class is considered invalid
|
||||
// (since they are broadcast/network addresses)
|
||||
//
|
||||
// - Added exclusion of private, reserved and/or local networks ranges
|
||||
//
|
||||
// - Made starting path slash optional (http://example.com?foo=bar)
|
||||
//
|
||||
// - Allow a dot (.) at the end of hostnames (http://example.com.)
|
||||
//
|
||||
// Compressed one-line versions:
|
||||
//
|
||||
// Javascript version
|
||||
//
|
||||
// /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
||||
//
|
||||
// PHP version
|
||||
//
|
||||
// _^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$_iuS
|
||||
//
|
||||
export default new RegExp(
|
||||
"^" +
|
||||
// protocol identifier
|
||||
"(?:(?:https?|ftp)://)" +
|
||||
// user:pass authentication
|
||||
"(?:\\S+(?::\\S*)?@)?" +
|
||||
"(?:" +
|
||||
// IP address exclusion
|
||||
// private & local networks
|
||||
"(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
|
||||
"(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
|
||||
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broacast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host name
|
||||
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
|
||||
// domain name
|
||||
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
|
||||
// TLD identifier
|
||||
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
|
||||
// TLD may end with dot
|
||||
"\\.?" +
|
||||
")" +
|
||||
// port number
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$", "i"
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user