first commit

This commit is contained in:
Pedro G. Galaviz
2019-11-28 20:55:32 -06:00
parent bde5ae77c1
commit 4a99a58085
63 changed files with 6227 additions and 174 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

5
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.DS_Store
node_modules
/dist
node_modules/
coverage/
# local env files
.env.local
@@ -14,6 +14,7 @@ yarn-error.log*
# Editor directories and files
.idea
.vscode
.tern-port
*.suo
*.ntvs*
*.njsproj

5
.npmignore Normal file
View File

@@ -0,0 +1,5 @@
docs/
docs-src/
tests/
coverage/
.babelrc

169
README.md
View File

@@ -1,29 +1,162 @@
# vue-mentions
# vue-highlights
## Project setup
```
npm install
Easy @mention, #hashtag and URL highlight for Vue 2.x
## Installation
You can install via npm or yarn:
```shell
npm install --save vue-highlights
yarn add vue-highlights
```
### Compiles and hot-reloads for development
```
npm run serve
And then import the component in your app:
```javascript
import Vue from 'vue'
import VueHighlights, { autoLink, autoHighlight } from 'vue-highlights'
// Install component
Vue.component(VueHighlights.name, VueHighlights)
```
### Compiles and minifies for production
##Usage
Let's create our first component:
```javascript
<template>
<vue-highlights
v-model="text"
:extractUrlsWithoutProtocol="true"
caretColor="#ccc"
placeholder="My custom placeholder..."
usernameClass="my-username-class"
hashtagClass="my-hash-class"
urlClass="my-url-class"
/>
</template>
<script>
export default {
name: 'MyComponent',
data () {
return {
text: text
}
}
}
</script>
```
npm run build
As you can see, the component accepts some props:
| Prop | Type | Description |
| ---- | ---- | -------- |
| value | String | The text to highlight (**v-model**). |
| extractUrlsWithoutProtocol | Boolean | As the name says, when active, the compoponet will try to match URLs even when a protocol (http://, https://) is not found. **Defaults to true** |
| caretColor | String | A valid HEX color (eg. #ccc, #ff4545). |
| placeholder | String | A placeholder to show when no text is entered. |
| usernameClass | String | The CSS class(es) that will be added to a @username match. |
| hashtagClass | String | The CSS class(es) that will be added to a #hashtag match. |
| urlClass | String | The CSS class(es) that will be added to a URL match. |
The exported component (**vue-highlights**) renders a text input that highlights all username, hashtag and URL matches. In order to work with this input some CSS classes should be attended, here's an example:
```css
.highlights__content {
position: relative;
}
.highlights__placeholder {
color: #ccc;
position: absolute;
top: 16px;
left: 16px;
z-index: -1;
}
.highlights__body-container {
border-radius: 5px;
border: 1px solid #eaeaea;
padding: 16px;
}
.highlights__body {
min-height: 60px;
}
.highlights {
color: #ff3b8e;
}
```
### Run your unit tests
```
npm run test:unit
With this we should get a working example.
As you can see when we first imported the package, 2 functions are also exported: **autoLink** and **autoHighlight**.
Both return a **String** value which contains our highlighted text. **autoLink** returns the matches found between **anchor** tags for links. **autoHighlight** returns the matches found between **span** tags for highlight only.
#### Examples
```javascript
import { autoLink, autoHighlight } from 'vue-highlights'
const text = 'my @username, my #hashtag and myurl.com'
const autoLinked = autoLink(text, {
extractUrlsWithoutProtocol: true, // Defaults to true
targetBlank: true, // Defauls to true, applies only in URLs
usernameClass: 'username-class',
usernameUrlBase: '/users/',
hashtagClass: 'hashtag-class',
hashtagUrlBase: '/myhashtags/',
urlClass: 'url-class'
})
/*
autoLinked:
my <a href="/users/username" title="@username" class="username-class"
data-username="username">@username</a>, my <a href="/myhashtags/hashtag"
title="#hashtag" class="hashtag-class" data-hashtag="hashtag">#hashtag</a>
and <a href="http://myurl.com" target="_blank" class="url-class">myurl.com</a>
*/
const autoHighlighted = autoHighlight(text, {
extractUrlsWithoutProtocol: true, // Defaults to true
usernameClass: 'username-class',
hashtagClass: 'hashtag-class',
urlClass: 'url-class'
})
/*
autoLinked:
my <span class="username-class">@username</span>, my <span class="hashtag-class">
#hashtag</span> and <span class="url-class">myurl.com</span>
*/
```
### Lints and fixes files
```
npm run lint
```
Now we can render our linked/highlighted text anywhere we like:
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
```javascript
<template>
<div class="my-linked-text">
<div v-html="text"></div>
</div>
</template>
<script>
import { autoLink } from 'vue-highlights'
const rawText = 'my @username, my #hashtag and myurl.com'
const autoLinked = autoLink(rawText) // Uses default options
export default {
name: 'MyComponent',
data () {
return {
text: autoLinked
}
}
}
</script>
```

10
dist/demo.html vendored Normal file
View File

@@ -0,0 +1,10 @@
<meta charset="utf-8">
<title>vue-highlights demo</title>
<script src="./vue-highlights.umd.js"></script>
<link rel="stylesheet" href="./vue-highlights.css">
<script>
console.log(vue-highlights)
</script>

1820
dist/vue-highlights.common.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/vue-highlights.common.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1830
dist/vue-highlights.umd.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/vue-highlights.umd.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/vue-highlights.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/vue-highlights.umd.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

90
docs-src/App.vue Normal file
View File

@@ -0,0 +1,90 @@
<template>
<div id="app">
<nav id="nav">
<div class="flex vcenter between container">
<div class="flex vcenter">
<img alt="Vue logo" src="@/assets/logo.png" class="mr-sm">
<router-link to="/">
<h1>vue-highlights</h1>
</router-link>
</div>
<div class="flex vcenter">
<router-link class="nav-item" :to="{ name: 'home' }">
Home
</router-link>
<router-link class="nav-item" :to="{ name: 'docs' }">
Documentation
</router-link>
<a class="nav-item" href="https://github.com/pggalaviz/vue-highlights" title="Github" target="_blank">
<div class="nav-icon">
<svg viewBox="0 0 16 16"><path d="M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z"></path></svg>
</div>
</a>
</div>
</div>
</nav>
<router-view/>
<footer id="footer" class="flex center py-md mt-lg">
<div class="text-center">
<div> &copy; 2019 Pedro G. Galaviz </div>
<a class="text-sm" href="http://pggalaviz.com">pggalaviz.com</a>
</div>
</footer>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="stylus">
@import '~@vars'
#app
padding-top: 80px
#nav
#footer
background-color: $color-background
width: 100%
#nav
border-bottom: 1px solid #eaeaea
height: 50px
left: 0
position: fixed
top: 0
z-index: 5
h1
line-height: 50px
font-size: 20px
margin: 0px
img
width: 24px
height: auto
.nav-item
color: $color-gray-8
font-size: 12px
font-weight: bold
margin-left: $space-base
transition: all .3s ease
&:hover
color: $color-brand
.nav-icon
fill: $color-gray-8
height: auto
margin-top: 4px
transition: all .3s ease
width: 20px
&:hover
fill: $color-brand
#footer
border-top: 1px solid #eaeaea
</style>

339
docs-src/Docs.vue Normal file
View File

@@ -0,0 +1,339 @@
<template>
<div id="docs">
<div class="container">
<h2>Documentation</h2>
<h3>Installation</h3>
<p>You can install via npm or yarn:</p>
<CodeSnippet lang="shell" :code="code1" />
<p>And then import the component in your app:</p>
<CodeSnippet lang="js" :code="code2" />
<h3>Usage</h3>
<p>Let's create our first component:</p>
<CodeSnippet lang="js" :code="code3" />
<p>As you can see, the component accepts some props:</p>
<table class="text-sm" style="width: 100%;">
<thead class="mb-sm">
<tr>
<th>Prop</th>
<th class="px-sm">Type</th>
<th colspan="2">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="props-name-col">
<div class="props-name">
extractUrlsWithoutProtocol
</div>
</td>
<td class="props-type-col px-sm">
<div class="props-type">
Boolean
</div>
</td>
<td class="props-desc-col">
<div class="props-desc">
As the name says, when active, the compoponet will try to match
URLs even when a protocol (http://, https://) is not found.
<b>Defaults to true</b>
</div>
</td>
</tr>
<tr>
<td class="props-name-col">
<div class="props-name">
caretColor
</div>
</td>
<td class="props-type-col px-sm">
<div class="props-type">
String
</div>
</td>
<td class="props-desc-col">
<div class="props-desc">
A valid HEX color (eg. #ccc, #ff4545).
</div>
</td>
</tr>
<tr>
<td class="props-name-col">
<div class="props-name">
placeholder
</div>
</td>
<td class="props-type-col px-sm">
<div class="props-type">
String
</div>
</td>
<td class="props-desc-col">
<div class="props-desc">
A placeholder to show when no text is entered.
</div>
</td>
</tr>
<tr>
<td class="props-name-col">
<div class="props-name">
usernameClass
</div>
</td>
<td class="props-type-col px-sm">
<div class="props-type">
String
</div>
</td>
<td class="props-desc-col">
<div class="props-desc">
The CSS class(es) that will be added to a @username match.
</div>
</td>
</tr>
<tr>
<td class="props-name-col">
<div class="props-name">
hashtagClass
</div>
</td>
<td class="props-type-col px-sm">
<div class="props-type">
String
</div>
</td>
<td class="props-desc-col">
<div class="props-desc">
The CSS class(es) that will be added to a #hashtag match.
</div>
</td>
</tr>
<tr>
<td class="props-name-col">
<div class="props-name">
urlClass
</div>
</td>
<td class="props-type-col px-sm">
<div class="props-type">
String
</div>
</td>
<td class="props-desc-col">
<div class="props-desc">
The CSS class(es) that will be added to a URL match.
</div>
</td>
</tr>
</tbody>
</table>
<p>
The exported component (vue-highlights) renders a text
input that highlights all username, hashtag and URL matches. In order to
work with this input some CSS classes should be attended, here's an
example:
</p>
<CodeSnippet lang="css" :code="code4" />
<p>With this we should get a working example.</p>
<p>As you can see when we first imported the package, 2 functions are also
exported: <b>autoLink</b> and <b>autoHighlight</b>.
</p>
<p>
Both return a <b>String</b> value which contains our highlighted text.
<b>autoLink</b> returns the matches found between <b>anchor</b> tags for
links.
<b>autoHighlight</b> returns the matches found between <b>span</b> tags for
highlight only.
</p>
<h5>Examples</h5>
<CodeSnippet lang="js" :code="code5" />
<p>Now we can render our linked/highlighted text anywhere we like:</p>
<CodeSnippet lang="js" :code="code6" />
</div>
</div>
</template>
<script>
import CodeSnippet from './components/CodeSnippet'
const code1 = `
npm install --save vue-highlights
yarn add vue-highlights
`
const code2 = `
import Vue from 'vue'
import VueHighlights, { autoLink, autoHighlight } from 'vue-highlights'
// Install component
Vue.component(VueHighlights.name, VueHighlights)
`
const code3 = `
<template>
<vue-highlights
v-model="text"
:extractUrlsWithoutProtocol="true"
caretColor="#ccc"
placeholder="My custom placeholder..."
usernameClass="my-username-class"
hashtagClass="my-hash-class"
urlClass="my-url-class"
/>
</template>
<script>
export default {
name: 'MyComponent',
data () {
return {
text: text
}
}
}
${'<'}/script>
`
const code4 = `
.highlights__content {
position: relative;
}
.highlights__placeholder {
color: #ccc;
position: absolute;
top: 16px;
left: 16px;
z-index: -1;
}
.highlights__body-container {
border-radius: 5px;
border: 1px solid #eaeaea;
padding: 16px;
}
.highlights__body {
min-height: 60px;
}
.highlights {
color: #ff3b8e;
}
`
const code5 = `
import { autoLink, autoHighlight } from 'vue-highlights'
const text = 'my @username, my #hashtag and myurl.com'
const autoLinked = autoLink(text, {
extractUrlsWithoutProtocol: true, // Defaults to true
targetBlank: true, // Defauls to true, applies only in URLs
usernameClass: 'username-class',
usernameUrlBase: '/users/',
hashtagClass: 'hashtag-class',
hashtagUrlBase: '/myhashtags/',
urlClass: 'url-class'
})
/*
autoLinked:
my <a href="/users/username" title="@username" class="username-class"
data-username="username">@username</a>, my <a href="/myhashtags/hashtag"
title="#hashtag" class="hashtag-class" data-hashtag="hashtag">#hashtag</a>
and <a href="http://myurl.com" target="_blank" class="url-class">myurl.com</a>
*/
const autoHighlighted = autoHighlight(text, {
extractUrlsWithoutProtocol: true, // Defaults to true
usernameClass: 'username-class',
hashtagClass: 'hashtag-class',
urlClass: 'url-class'
})
/*
autoLinked:
my <span class="username-class">@username</span>, my <span class="hashtag-class">
#hashtag</span> and <span class="url-class">myurl.com</span>
*/
`
const code6 = `
<template>
<div class="my-linked-text">
<div v-html="text"></div>
</div>
</template>
<script>
import { autoLink } from 'vue-highlights'
const rawText = 'my @username, my #hashtag and myurl.com'
const autoLinked = autoLink(rawText) // Uses default options
export default {
name: 'MyComponent',
data () {
return {
text: autoLinked
}
}
}
${'<'}/script>
`
export default {
name: 'Docs',
components: { CodeSnippet },
data () {
return {
code1,
code2,
code3,
code4,
code5,
code6
}
}
}
</script>
<style lang="stylus">
@import '~@vars'
.props-name-col
width: 30%
.props-type-col
width: 10%
.props-name
color: $color-brand
font-weight: 500
.props-type
color: $color-gray-6
font-weight: 600
</style>

151
docs-src/Home.vue Normal file
View File

@@ -0,0 +1,151 @@
<template>
<div id="home" class="text-center">
<img id="logo" alt="Vue logo" src="./assets/logo.png">
<h1>vue-highlights</h1>
<div id="description" class="mb-md">
<b>Easy mention, hashtag and URL highlight for Vue 2.x</b>
</div>
<div class="flex center mb-md text-sm">
<div id="install" class="pa-md font-mono content-container">
npm install --save vue-highlights
</div>
</div>
<div class="flex center text-md">
<vue-highlights
v-model="text"
class="content-container"
:placeholder="placeholder"
:caretColor="caretColor"
:extractUrlsWithoutProtocol="options.extractUrlsWithoutProtocol"
/>
</div>
<div class="flex center my-md">
<div id="options" class="content-container">
<h4>Options (props)</h4>
<div class="flex center">
<label for="ep" class="mr-lg cursor-pointer">
<input id="ep" type="checkbox" v-model="options.extractUrlsWithoutProtocol">
extractUrlsWithoutProtocol
</label>
<label for="tb" class="cursor-pointer">
<input id="tb" type="checkbox" v-model="options.targetBlank">
targetBlank
</label>
</div>
<div class="flex center mt-sm text-left relative">
<div class="mr-md col-50">
<label for="uc" class="label cursor-pointer">
usernameClass
</label>
<input id="uc" type="text" class="input" v-model="options.usernameClass">
</div>
<div class="col-50">
<label for="ut" class="label cursor-pointer">
usernameUrlBase
</label>
<div>
<input id="ut" type="text" class="input" v-model="options.usernameUrlBase">
</div>
</div>
</div>
<div class="flex center mt-sm text-left">
<div class="mr-md col-50">
<label for="hc" class="label cursor-pointer">
hashtagClass
</label>
<div>
<input id="hc" type="text" class="input" v-model="options.hashtagClass">
</div>
</div>
<div class="col-50">
<label for="ht" class="label cursor-pointer">
hashtagUrlBase
</label>
<div>
<input id="ht" type="text" class="input" v-model="options.hashtagUrlBase">
</div>
</div>
</div>
<div class="flex center mt-sm text-left">
<div class="mr-md col-50">
<label for="urc" class="label cursor-pointer">
urlClass
</label>
<div>
<input id="urc" type="text" class="input" v-model="options.urlClass">
</div>
</div>
<div class="col-50">
<label for="cc" class="label cursor-pointer">
caretColor
</label>
<div>
<input id="cc" type="text" class="input" v-model="caretColor">
</div>
</div>
</div>
</div>
</div>
<div class="column center vcenter">
<div class="mb-md">
<h4>HTML with links:</h4>
<div class="content-container text-md" v-html="$autoLink(text, options)"></div>
</div>
<div class="mb-md">
<h4>Text with links:</h4>
<div class="content-container"> {{ $autoLink(text, options) }}</div>
</div>
<div class="mb-md">
<h4>Model text:</h4>
<div class="content-container"> {{ text }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
data () {
return {
placeholder: 'Write something here, include @mentions, #hashtags and URLs...',
text: 'Hi there! @pggalaviz #vue pggalaviz.com',
caretColor: '#ff3b8e',
options: {
targetBlank: true,
extractUrlsWithoutProtocol: true,
usernameClass: 'highlights username',
usernameUrlBase: '#/',
hashtagClass: 'highlights hashtag',
hashtagUrlBase: '#/hashtag/',
urlClass: 'highlights url'
}
}
}
}
</script>
<style lang="stylus">
@import '~@vars'
#logo
width: 50px
height: auto
#install
background-color: lighten($color-border, 60%)
line-height: 1
</style>

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,66 @@
<template>
<div class="code-snippet box relative flex">
<div class="language">{{ lang }}</div>
<div class="line-numbers">
<div class="line-number" v-for="n in lineCount" :key="n">{{ n }}</div>
</div>
<div class="render" v-html="result"></div>
</div>
</template>
<script>
import hljs from 'highlight.js'
export default {
name: 'CodeSnippet',
props: {
code: String,
lang: String
},
computed: {
result () {
const highlighted = hljs.highlight(this.lang, this.code.trim())
return highlighted.value
},
lineCount () {
const str = this.result
let length = 0
for (var i = 0; i < str.length; ++i) {
if (str[i] === '\n') {
length++
}
}
return length + 1
}
}
}
</script>
<style lang="stylus">
@import '~@vars'
.code-snippet
padding: 0px !important
margin-bottom: $space-base
font-family: 'Roboto Mono', monospace
.line-numbers,
.render
padding: 12px 12px
.line-numbers
color: $color-gray-4
border-radius: 2px 0 0 2px
border-right: 1px solid $color-border
.render
white-space: pre
.language
position: absolute
top: 0
right: 0
background: $color-gray-1
color: $color-gray-9
padding: 3px 12px
border-radius: 0 5px 0 4px
</style>

29
docs-src/main.js Normal file
View File

@@ -0,0 +1,29 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import VueHighlights, { autoLink, autoHighlight } from '../src'
import App from './App.vue'
import Home from './Home.vue'
import Docs from './Docs.vue'
import './styles/main.styl'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/', name: 'home', component: Home },
{ path: '/docs', name: 'docs', component: Docs },
{ path: '*', component: Home }
]
})
Vue.component(VueHighlights.name, VueHighlights)
Vue.prototype.$autoLink = autoLink
Vue.prototype.$autoHighlight = autoHighlight
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')

131
docs-src/styles/base.styl Normal file
View File

@@ -0,0 +1,131 @@
*, *:after, *:before
box-sizing: border-box
html, body
width : 100%
height: 100%
html
font-size: 100%
text-size-adjust: 100%
body
overflow-x: hidden
overflow-y: scroll
background-color: $color-background
min-height: 100%
font-family: $font-helvetica
font-size: $font-md
line-height: $line-height-base
font-weight: $font-weight-base
color: $font-color-base
text-rendering: optimizeLegibility
scroll-behavior: smooth
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
// Selection
::-moz-selection
background-color: $color-brand-lighter
color: $color-white
::selection
background-color: $color-brand-lighter
color: $color-white
// Placeholder
::-webkit-input-placeholder
color inherit
opacity .7
::-moz-placeholder
color inherit
opacity .7
:-ms-input-placeholder
color inherit !important
opacity .7 !important
::-ms-input-placeholder
color inherit
opacity .7
::placeholder
color inherit
opacity .7
p
margin: 0
&:not(:last-child)
margin-bottom: $space-base
// Links
a
line-height: inherit
transition: color 0.2s ease, border-bottom-color 0.2s ease
border: none
font-family: inherit
color: $color-link
text-decoration: none
cursor: pointer
&:focus
outline: none
&:hover
color: $color-link-hover
img
border: 0
&.router-exact-active // vue-router
color: $color-gray-6
h1, h2, h3, h4, h5
color: $font-color-base
// Images
img
width: 100%
max-width: 100%
height: auto
pre
background-color: lighten($color-border, 60%)
padding: $space-base
line-height: 1.5
table
width: 100%
margin-bottom: $space-sm
th
font-weight: 800
th, td
padding-bottom: 16px
text-align: center
text-align: left
vertical-align: middle
// layout
.flex
.column
display: flex
&.center
justify-content: center
&.between
justify-content: space-between
&.vcenter
align-items: center
.column
flex-direction: column
.col-50
width: 50%
.col-25
width: 25%
.relative
position: relative
.container
width: 960px
max-width: 96%
margin: 0 auto
.content-container
width: 560px
max-width: 96%
.cursor-pointer
cursor: pointer

57
docs-src/styles/main.styl Normal file
View File

@@ -0,0 +1,57 @@
@import 'variables'
@import 'reset'
@import 'base'
@import 'spacing'
@import 'typo'
// Mentions
.highlights__content
position: relative
.highlights__placeholder
color: #ccc
position: absolute
top: $space-base
left: $space-base
z-index: -1
.box,
.highlights__body-container
border-radius: 5px
box-shadow: 0 5px 15px 0 rgba(80,86,98,.1), 0 2px 4px 0 rgba(199,202,209,.4)
padding: $space-base
.highlights__body
min-height: 60px
.highlights
color: $color-primary
.label
display: block
margin-bottom: 3px
.input
background-color: $color-background
background-image: none
border: 1px solid $color-gray-1
border-radius: 4px
color: $color-gray-9
cursor: text
display: block
font-size: 12px
height: 32px
line-height: 32px
max-width: 100%
min-width: 100%
padding: 2px 8px
position: relative
transition: all .2s
width: 100%
&:hover
border-color: $color-gray-4
&:focus,
&.focus
border-color: $color-brand
outline: none

212
docs-src/styles/reset.styl Normal file
View File

@@ -0,0 +1,212 @@
// Custom browser reset
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, select
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, main
display: block
html
font-family: sans-serif
-ms-text-size-adjust: 100%
-webkit-text-size-adjust: 100%
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
figcaption,
figure
display: block
hr
box-sizing: content-box
height: 0
overflow: visible
pre
font-family: monospace, monospace
font-size: 1em
a
background-color: transparent
-webkit-text-decoration-skip: objects
&:hover,
&:active
outline-width: 0
abbr[title]
border-bottom: none
text-decoration: underline
text-decoration: underline dotted
b,
strong
font-weight: bolder
code,
kbd,
samp
font-family: monospace, monospace
font-size: 1em
dfn
font-style: italic
mark
background-color: #ff0
color: #000
small
font-size: 80%
sub,
sup
font-size: 75%
line-height: 0
position: relative
vertical-align: baseline
sub
bottom: -0.25em
sup
top: -0.5em
audio,
video,
canvas
display: inline-block
audio:not([controls])
display: none
height: 0
img
border-style: none
svg:not(:root)
overflow: hidden
button,
input,
optgroup,
select,
textarea
font-family: sans-serif
font-size: 100%
line-height: 1
margin: 0
button
overflow: visible
button,
select
text-transform: none
button,
html [type="button"],
[type="reset"],
[type="submit"]
-webkit-appearance: button
button,
[type="button"],
[type="reset"],
[type="submit"]
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner
border-style: none
padding: 0
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring
outline: 1px dotted ButtonText
input
overflow: visible
[type="checkbox"],
[type="radio"]
box-sizing: border-box
padding: 0
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button
height: auto
[type="search"]
-webkit-appearance: textfield
outline-offset: -2px
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration
-webkit-appearance: none
::-webkit-file-upload-button
-webkit-appearance: button
font: inherit
legend
box-sizing: border-box
display: table
max-width: 100%
padding: 0
color: inherit
white-space: normal
progress
display: inline-block
vertical-align: baseline
textarea
overflow: auto
summary
display: list-item
template
display: none
[hidden]
display: none

View File

@@ -0,0 +1,32 @@
// Spacing
for $space, $value in $global-spaces
.pa-{$space}
padding: $value
.pl-{$space}
padding-left: $value
.pr-{$space}
padding-right: $value
.pt-{$space}
padding-top: $value
.pb-{$space}
padding-bottom: $value
.px-{$space}
@extends .pl-{$space}, .pr-{$space}
.py-{$space}
@extends .pt-{$space}, .pb-{$space}
.ma-{$space}
margin: $value
.ml-{$space}
margin-left: $value
.mr-{$space}
margin-right: $value
.mt-{$space}
margin-top: $value
.mb-{$space}
margin-bottom: $value
.mx-{$space}
@extends .ml-{$space}, .mr-{$space}
.my-{$space}
@extends .mt-{$space}, .mb-{$space}

74
docs-src/styles/typo.styl Normal file
View File

@@ -0,0 +1,74 @@
// Typo
// Headings
// ========
$--typo-header-font-family ?= $font-helvetica
$--typo-header-font-weight ?= 800
$--typo-header-font-style ?= normal
$--typo-header-color ?= inherit
$--typo-header-line-height ?= $global-line-height-heading
$--typo-header-margin-bottom ?= $space-base/2
strong, b
font-weight: $font-weight-bold
line-height: inherit
h1, .h1,
h2, .h2,
h3, .h3,
h4, .h4,
h5, .h5,
h6, .h6
font-family: $--typo-header-font-family
font-style: $--typo-header-font-style
font-weight: $--typo-header-font-weight
color: $--typo-header-color
text-rendering: optimizeLegibility
letter-spacing: -1px
line-height: $--typo-header-line-height
margin-bottom: $--typo-header-margin-bottom
small
line-height: 0
color: $color-gray-light
a
color: inherit
a:hover
color: inherit
h1, .h1
font-size: $font-h1
h2, .h2
font-size: $font-h2
h3, .h3
font-size: $font-h3
h4, .h4
font-size: $font-h4
h5, .h5
font-size: $font-h5
h6, .h6
font-size: $font-h6
text-transform: uppercase
color: $color-gray
letter-spacing: 0px
// size
.text-xs
font-size: $font-xs
.text-sm
font-size: $font-sm
.text-md
font-size: $font-md
.text-lg
font-size: $font-lg
.text-xl
font-size: $font-xl
.text-left
text-align: left
.text-center
text-align: center
.text-right
text-align: right
.font-mono
font-family: $font-mono

View File

@@ -0,0 +1,92 @@
// Variables
// Color
$color-primary ?= #ff3b8e
$color-brand ?= $color-primary
$color-brand-light ?= lighten($color-brand, 25%)
$color-brand-lighter ?= lighten($color-brand, 50%)
$color-brand-dark ?= darken($color-brand, 25%)
$color-brand-darker ?= darken($color-brand, 50%)
// Grays
$color-gray ?= #828c8f
$color-gray-1 ?= lighten($color-gray, 80%)
$color-gray-2 ?= lighten($color-gray, 70%)
$color-gray-3 ?= lighten($color-gray, 60%)
$color-gray-4 ?= lighten($color-gray, 50%)
$color-gray-5 ?= lighten($color-gray, 40%)
$color-gray-6 ?= lighten($color-gray, 30%)
$color-gray-7 ?= lighten($color-gray, 20%)
$color-gray-8 ?= lighten($color-gray, 10%)
$color-gray-9 ?= $color-gray
$color-gray-10 ?= darken($color-gray, 10%)
$color-gray-11 ?= darken($color-gray, 20%)
$color-gray-12 ?= darken($color-gray, 30%)
$color-gray-13 ?= darken($color-gray, 40%)
$color-gray-14 ?= darken($color-gray, 50%)
$color-gray-15 ?= darken($color-gray, 60%)
$color-gray-16 ?= darken($color-gray, 70%)
// Other colors
$color-black ?= #0a0a0a
$color-white ?= #fefefe
$color-blue ?= #00B0E9
$color-red ?= #f56c6c
$color-yellow ?= #f5ad58
$color-green ?= #67C23A
$color-pink ?= #D3529B
// App
$color-danger ?= $color-red
$color-warning ?= $color-yellow
$color-success ?= $color-green
$color-info ?= $color-blue
$color-background ?= $color-white
$color-border ?= #eaeaea
// Links
$color-link ?= $color-brand
$color-link-hover ?= $color-gray-8
$color-link-active ?= $color-gray-5
// Space
$space-base ?= 16px
$space-sm ?= 8px
$space-lg ?= 24px
$global-spaces ?= {
sm: $space-base * .5,
md: $space-base,
lg: $space-base * 1.25,
}
// Typo
$font-helvetica ?= Helvetica neue, Helvetica, Arial, sans-serif
$font-mono ?= Menlo, Monaco, Consolas, monospace
$font-default ?= $font-helvetica
$font-base ?= 14px
$font-xs ?= 10px
$font-sm ?= 12px
$font-md ?= $font-base
$font-lg ?= 16px
$font-xl ?= 18px
$font-h1 ?= 28px
$font-h2 ?= 24px
$font-h3 ?= 20px
$font-h4 ?= 18px
$font-h5 ?= 16px
$font-h6 ?= 14px
$font-weight-base ?= 400
$font-weight-light ?= 300
$font-weight-medium ?= 600
$font-weight-bold ?= 700
$font-weight-bolder ?= 800
$font-color-base ?= $color-gray-12
$font-color-light ?= $color-gray-4
$line-height-base ?= 1.5
$line-height-heading ?= 1.2

File diff suppressed because one or more lines are too long

BIN
docs/img/logo.82b9c7a5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

1
docs/index.html Normal file
View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel="shortcut icon" href=./logo.png><link href=//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/styles/atom-one-light.min.css rel=stylesheet><title>vue-highlights</title><link href=css/app.0d018692.css rel=preload as=style><link href=js/app.205581fc.js rel=preload as=script><link href=js/chunk-vendors.173b11bb.js rel=preload as=script><link href=css/app.0d018692.css rel=stylesheet></head><body><noscript><strong>We're sorry but vue-highlights doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=js/chunk-vendors.173b11bb.js></script><script src=js/app.205581fc.js></script></body></html>

2
docs/js/app.205581fc.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

14
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{
"name": "vue-mentions",
"name": "vue-highlights",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
@@ -4080,7 +4080,8 @@
"core-js": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.4.4.tgz",
"integrity": "sha512-vKea1DrcLA80Hrfc7AQgfoGvEaByfR5mH08t+zuWOWY94TFBmabdEL56mUbcijvadG9RxsXR2gUUFrfj4/iTcA=="
"integrity": "sha512-vKea1DrcLA80Hrfc7AQgfoGvEaByfR5mH08t+zuWOWY94TFBmabdEL56mUbcijvadG9RxsXR2gUUFrfj4/iTcA==",
"dev": true
},
"core-js-compat": {
"version": "3.4.4",
@@ -11117,8 +11118,7 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"q": {
"version": "1.5.1",
@@ -13352,7 +13352,8 @@
"vue": {
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==",
"dev": true
},
"vue-eslint-parser": {
"version": "5.0.0",
@@ -13421,7 +13422,8 @@
"vue-router": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.3.tgz",
"integrity": "sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ=="
"integrity": "sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ==",
"dev": true
},
"vue-style-loader": {
"version": "4.1.2",

View File

@@ -1,17 +1,21 @@
{
"name": "vue-mentions",
"name": "vue-highlights",
"version": "0.1.0",
"private": true,
"private": false,
"description": "Easy @mention, #hashtag and URL highlight for Vue 2.x",
"author": "Pedro G. Galaviz <hello@pggalaviz.com>",
"scripts": {
"build": "vue-cli-service build --target lib --dest dist --name vue-highlights src/index.js",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test": "npm run test:unit",
"build-docs": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
"prepublish": "npm run test && npm run build"
},
"main": "dist/vue-highlights.common.js",
"unpkg": "dist/vue-highlights.umd.min.js",
"dependencies": {
"core-js": "^3.4.3",
"vue": "^2.6.10",
"vue-router": "^3.1.3"
"punycode": "^2.1.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
@@ -22,10 +26,32 @@
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "1.0.0-beta.29",
"babel-eslint": "^10.0.3",
"core-js": "^3.4.3",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"highlight.js": "^9.16.2",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vue-template-compiler": "^2.6.10"
},
"bugs": {
"url": "https://github.com/pggalaviz/vue-highlights/issues"
},
"homepage": "https://github.com/pggalaviz/vue-highlights#readme",
"keywords": [
"vue",
"highlight",
"highlights",
"mention",
"mentions",
"hashtag",
"hashtags"
],
"licence": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/pggalaviz/vue-highlights.git"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -4,12 +4,13 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>vue-mentions</title>
<link rel="shortcut icon" href="./logo.png">
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/styles/atom-one-light.min.css" rel="stylesheet">
<title>vue-highlights</title>
</head>
<body>
<noscript>
<strong>We're sorry but vue-mentions doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
<strong>We're sorry but vue-highlights doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,19 +0,0 @@
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>

View File

@@ -1,59 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" target="_blank" rel="noopener">unit-jest</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="stylus">
h3
margin 40px 0 0
ul
list-style-type none
padding 0
li
display inline-block
margin 0 10px
a
color #42b983
</style>

146
src/index.js Normal file
View File

@@ -0,0 +1,146 @@
import { link, highlight, setCaretPosition } from './utils'
export function autoHighlight (text, options) {
return highlight(text, options)
}
export function autoLink (text, options) {
return link(text, options)
}
export default {
name: 'VueHighlights',
props: {
extractUrlsWithoutProtocol: {
type: Boolean,
default: true
},
caretColor: {
type: String,
default: '#ccc'
},
placeholder: {
type: String,
default: `What's Happening?`
},
value: String
},
data () {
return {
focused: false,
body: ''
}
},
computed: {
showPlaceholder () {
return !this.body.replace(/^\s*\n/gm, '').length
},
computedBody () {
return highlight(this.body, {
extractUrlsWithoutProtocol: this.extractUrlsWithoutProtocol
})
}
},
methods: {
getCaretPos () {
const parent = this.$refs.mbody
const selection = window.getSelection()
let node = selection.focusNode
let charCount = selection.focusOffset
while (node) {
if (node === parent) break
if (node.previousSibling) {
node = node.previousSibling
charCount += node.textContent.length
} else {
node = node.parentNode
if (node === null) break
}
}
return charCount
},
setCaretPos (caretPosition) {
setCaretPosition(this.$refs.mbody, caretPosition)
},
clear () {
this.$refs.mbody.innerText = ''
this.body = ''
},
onKeyUp (e) {
let caretPosition = this.getCaretPos()
if (e.keyCode === 13) { // Enter key
caretPosition++
}
this.body = e.target.innerText
this.$emit('input', this.body)
this.$nextTick(() => {
this.setCaretPos(caretPosition)
})
},
onFocus (e) {
this.focused = true
this.$emit('focus', e)
},
onBlur (e) {
this.focused = false
this.$emit('blur', e)
}
},
render (h) {
const placeHolder = this.showPlaceholder ? h('div', {
attrs: {
id: 'mplaceholder'
},
staticClass: 'highlights__placeholder'
}, this.placeholder) : null
const input = {
ref: 'mbody',
staticClass: 'highlights__body',
style: {
'text-align': 'initial',
outline: 'currentcolor none medium',
'user-select': 'text',
'white-space': 'pre-wrap',
'overflow-wrap': 'break-word',
'caret-color': `${this.caretColor}`
},
attrs: {
'aria-label': this.placeHolder,
'aria-autocomplete': 'list',
'aria-describedby': 'mplaceholder',
'aria-multiline': 'true',
contenteditable: true,
role: 'textbox',
spellCheck: true,
tabindex: 0
},
domProps: {
innerHTML: this.computedBody
},
on: {
focus: this.onFocus,
blur: this.onBlur,
keyup: this.onKeyUp
}
}
return h('div', {
staticClass: 'highlights__container',
style: {
position: 'relative'
}
}, [
h('div', {
staticClass: 'highlights__content'
}, [
placeHolder,
h('div', {
staticClass: 'highlights__body-container'
}, [
h('div', input)
])
])
])
}
}

View File

@@ -1,10 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')

View File

@@ -1,27 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
routes
})
export default router

View File

@@ -0,0 +1,55 @@
// Inserts a <span> tag with given css classes around matched url,
// mentions or hashtags in text.
import { htmlEscape, clone, stringSupplant } from './helpers'
const DEFAULT_USERNAME_CLASS = 'highlights username'
const DEFAULT_HASHTAG_CLASS = 'highlights hashtag'
const DEFAULT_URL_CLASS = 'highlights url'
export default function (text, entities, opts) {
let result = ''
let beginIndex = 0
const options = clone(opts || {})
const usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS
const hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS
const urlClass = options.urlClass || DEFAULT_URL_CLASS
// sort entities by start index
entities.sort(function (a, b) {
return a.indices[0] - b.indices[0]
})
for (let i = 0; i < entities.length; i++) {
const entity = entities[i]
result += htmlEscape(text.substring(beginIndex, entity.indices[0]))
if (entity.url) {
let url = htmlEscape(entity.url)
result += _insertTag(url, urlClass)
} else if (entity.username) {
const at = text.substring(entity.indices[0], entity.indices[0] + 1)
const user = htmlEscape(entity.username)
result += _insertTag(`${at}${user}`, usernameClass)
} else if (entity.hashtag) {
const hash = text.substring(entity.indices[0], entity.indices[0] + 1)
const tag = htmlEscape(entity.hashtag)
result += _insertTag(`${hash}${tag}`, hashtagClass)
}
beginIndex = entity.indices[1]
}
result += htmlEscape(text.substring(beginIndex, text.length))
return result
}
// =================
// Private Functions
// =================
function _insertTag (text, classes = '') {
const opts = {
text: text,
attr: `class="${classes}"`
}
return stringSupplant('<span #{attr}>#{text}</span>', opts)
}

47
src/utils/autoLink.js Normal file
View File

@@ -0,0 +1,47 @@
// Inserts an <a> or <router-link> tag with given css classes around
// matched url, mentions or hashtags in text.
import { clone, htmlEscape } from './helpers'
import extractHtmlAttrs from './extractHtmlAttrs'
import linkToUrl from './linkToUrl'
import linkToMention from './linkToMention'
import linkToHashtag from './linkToHashtag'
const DEFAULT_USERNAME_CLASS = 'highlights username'
const DEFAULT_HASHTAG_CLASS = 'highlights hashtag'
const DEFAULT_URL_CLASS = 'highlights url'
export default function (text, entities, opts) {
let options = clone(opts || {})
options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS
options.usernameUrlBase = options.usernameUrlBase || '/'
options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS
options.hashtagUrlBase = options.hashtagUrlBase || '/hashtag/'
options.urlClass = options.urlClass || DEFAULT_URL_CLASS
options.htmlAttrs = extractHtmlAttrs(options)
options.invisibleTagAttrs = options.invisibleTagAttrs || "style='position:absolute;left:-9999px;'"
let result = ''
let beginIndex = 0
// sort entities by start index
entities.sort(function (a, b) {
return a.indices[0] - b.indices[0]
})
for (let i = 0; i < entities.length; i++) {
const entity = entities[i]
result += htmlEscape(text.substring(beginIndex, entity.indices[0]))
if (entity.url) {
result += linkToUrl(entity, text, options)
} else if (entity.username) {
result += linkToMention(entity, text, options)
} else if (entity.hashtag) {
result += linkToHashtag(entity, text, options)
}
beginIndex = entity.indices[1]
}
result += htmlEscape(text.substring(beginIndex, text.length))
return result
}

20
src/utils/extract.js Normal file
View File

@@ -0,0 +1,20 @@
// Returns an Indexed Array with URL, mention and hashtag
// entities found in text.
import extractMentions from './extractMentions'
import extractHashtags from './extractHashtags'
import extractUrls from './extractUrls'
import removeOverlappingEntities from './removeOverlappingEntities'
export default function (text, options) {
const entities = extractUrls(text, options)
.concat(extractMentions(text))
.concat(extractHashtags(text))
if (entities.length === 0) {
return []
}
removeOverlappingEntities(entities)
return entities
}

View File

@@ -0,0 +1,26 @@
// Extracts Hashtags from text.
import { endHashtagMatch, hashSigns, validHashtag } from './regex'
export default function (text) {
if (!text || !text.match(hashSigns)) {
return []
}
let tags = []
text.replace(validHashtag, function (match, before, hash, hashText, offset, chunk) {
const after = chunk.slice(offset + match.length)
if (after.match(endHashtagMatch)) {
return
}
const startPosition = offset + before.length
const endPosition = startPosition + hashText.length + 1
tags.push({
hashtag: hashText,
indices: [startPosition, endPosition]
})
})
return tags
}

View File

@@ -0,0 +1,39 @@
const BOOLEAN_ATTRIBUTES = {
disabled: true,
readonly: true,
multiple: true,
checked: true
}
// Options which should not be passed as HTML attributes
const OPTIONS_NOT_ATTRIBUTES = {
urlClass: true,
usernameClass: true,
hashtagClass: true,
usernameUrlBase: true,
hashtagUrlBase: true,
targetBlank: true,
urlTarget: true,
invisibleTagAttrs: true,
linkAttributeBlock: true,
htmlEscapeNonEntities: true,
extractUrlsWithoutProtocol: true
}
export default function (options) {
const htmlAttrs = {}
for (const k in options) {
let v = options[k]
if (OPTIONS_NOT_ATTRIBUTES[k]) {
continue
}
if (BOOLEAN_ATTRIBUTES[k]) {
v = v ? k : null
}
if (v == null) {
continue
}
htmlAttrs[k] = v
}
return htmlAttrs
}

View File

@@ -0,0 +1,25 @@
// Extracts mentions from text.
import { atSigns, endMentionMatch, validMention } from './regex'
export default function (text) {
if (!text || !text.match(atSigns)) {
return []
}
const mentions = []
text.replace(validMention, function (match, before, atSign, mentionText, offset, chunk) {
const after = chunk.slice(offset + match.length)
if (!after.match(endMentionMatch)) {
const startPosition = offset + before.length
const endPosition = startPosition + mentionText.length + 1
mentions.push({
username: mentionText,
indices: [startPosition, endPosition]
})
}
})
return mentions
}

81
src/utils/extractUrls.js Normal file
View File

@@ -0,0 +1,81 @@
// Extracts URLs from text
import { extractUrl, validAsciiDomain } from './regex'
import idna from './idna'
const DEFAULT_PROTOCOL = 'https://'
const DEFAULT_PROTOCOL_OPTIONS = { extractUrlsWithoutProtocol: true }
const MAX_URL_LENGTH = 4096
const invalidUrlWithoutProtocolPrecedingChars = /[-_./]$/
function isValidUrl (url, protocol, domain) {
let urlLength = url.length
const punycodeEncodedDomain = idna.toAscii(domain)
if (!punycodeEncodedDomain || !punycodeEncodedDomain.length) {
return false
}
urlLength = urlLength + punycodeEncodedDomain.length - domain.length
return protocol.length + urlLength <= MAX_URL_LENGTH
}
const extractUrlsWithIndices = function (text, options = DEFAULT_PROTOCOL_OPTIONS) {
if (!text || (options.extractUrlsWithoutProtocol ? !text.match(/\./) : !text.match(/:/))) {
return []
}
const urls = []
while (extractUrl.exec(text)) {
const before = RegExp.$2
let url = RegExp.$3
const protocol = RegExp.$4
const domain = RegExp.$5
const path = RegExp.$7
let endPosition = extractUrl.lastIndex
const startPosition = endPosition - url.length
if (!isValidUrl(url, protocol || DEFAULT_PROTOCOL, domain)) {
continue
}
// extract ASCII-only domains.
if (!protocol) {
if (!options.extractUrlsWithoutProtocol || before.match(invalidUrlWithoutProtocolPrecedingChars)) {
continue
}
let lastUrl = null
let asciiEndPosition = 0
domain.replace(validAsciiDomain, function (asciiDomain) {
const asciiStartPosition = domain.indexOf(asciiDomain, asciiEndPosition)
asciiEndPosition = asciiStartPosition + asciiDomain.length
lastUrl = {
url: asciiDomain,
indices: [startPosition + asciiStartPosition, startPosition + asciiEndPosition]
}
urls.push(lastUrl)
})
// no ASCII-only domain found. Skip the entire URL.
if (lastUrl == null) {
continue
}
// lastUrl only contains domain. Need to add path and query if they exist.
if (path) {
lastUrl.url = url.replace(domain, lastUrl.url)
lastUrl.indices[1] = endPosition
}
} else {
urls.push({
url: url,
indices: [startPosition, endPosition]
})
}
}
return urls
}
export default extractUrlsWithIndices

32
src/utils/helpers.js Normal file
View File

@@ -0,0 +1,32 @@
const HTML_ENTITIES = {
'&': '&amp;',
'>': '&gt;',
'<': '&lt;',
'"': '&quot;',
"'": '&#39;'
}
export function htmlEscape (text) {
return (
text &&
text.replace(/[&"'><]/g, function (character) {
return HTML_ENTITIES[character]
})
)
}
export function clone (o) {
const r = {}
for (const k in o) {
if (o.hasOwnProperty(k)) {
r[k] = o[k]
}
}
return r
}
export function stringSupplant (str, map) {
return str.replace(/#\{(\w+)\}/g, function (match, name) {
return map[name] || ''
})
}

30
src/utils/idna.js Normal file
View File

@@ -0,0 +1,30 @@
import punycode from 'punycode'
import { validAsciiDomain } from './regex'
const MAX_DOMAIN_LABEL_LENGTH = 63
const PUNYCODE_ENCODED_DOMAIN_PREFIX = 'xn--'
// This is an extremely lightweight implementation of domain name validation according to RFC 3490
// Our regexes handle most of the cases well enough
// See https://tools.ietf.org/html/rfc3490#section-4.1 for details
const idna = {
toAscii: function (domain) {
if (domain.substring(0, 4) === PUNYCODE_ENCODED_DOMAIN_PREFIX && !domain.match(validAsciiDomain)) {
// Punycode encoded url cannot contain non ASCII characters
return
}
const labels = domain.split('.')
for (let i = 0; i < labels.length; i++) {
const label = labels[i]
const punycodeEncodedLabel = punycode.toASCII(label)
if (punycodeEncodedLabel.length < 1 || punycodeEncodedLabel.length > MAX_DOMAIN_LABEL_LENGTH) {
// DNS label has invalid length
return
}
}
return labels.join('.')
}
}
export default idna

56
src/utils/index.js Normal file
View File

@@ -0,0 +1,56 @@
import extract from './extract'
import autoLink from './autoLink'
import autoHighlight from './autoHighlight'
const defaultOptions = {
targetBlank: true,
extractUrlsWithoutProtocol: true
}
export function link (text, options = defaultOptions) {
const entities = extract(text, options)
return autoLink(text, entities, options)
}
export function highlight (text, options = defaultOptions) {
const entities = extract(text, options)
return autoHighlight(text, entities, options)
}
export function createRange (node, chars, range) {
if (!range) {
range = document.createRange()
range.selectNode(node)
range.setStart(node, 0)
}
if (chars.count === 0) {
range.setEnd(node, chars.count)
} else if (node && chars.count > 0) {
if (node.nodeType === 3) {
if (node.textContent.length < chars.count) {
chars.count -= node.textContent.length
} else {
range.setEnd(node, chars.count)
chars.count = 0
}
} else {
for (let i = 0; i < node.childNodes.length; i++) {
range = createRange(node.childNodes[i], chars, range)
if (chars.count === 0) break
}
}
}
return range
}
export function setCaretPosition (node, caretPosition) {
if (caretPosition >= 0) {
const range = createRange(node, { count: caretPosition })
const selection = window.getSelection()
if (range) {
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
}
}
}

View File

@@ -0,0 +1,22 @@
// Converts hashtag entity to an html anchor tag.
import { clone, htmlEscape } from './helpers'
import linkToText from './linkToText'
const rtlChars = /[\u0600-\u06FF]|[\u0750-\u077F]|[\u0590-\u05FF]|[\uFE70-\uFEFF]/gm
export default function (entity, text, options) {
const hash = text.substring(entity.indices[0], entity.indices[0] + 1)
const hashtag = htmlEscape(entity.hashtag)
const attrs = clone(options.htmlAttrs || {})
attrs.href = options.hashtagUrlBase + hashtag
attrs.title = `#${hashtag}`
attrs['class'] = options.hashtagClass
attrs['data-hashtag'] = hashtag
if (hashtag.charAt(0).match(rtlChars)) {
attrs['class'] += ' rtl'
}
return linkToText(entity, `${hash}${hashtag}`, attrs, options)
}

View File

@@ -0,0 +1,17 @@
// Converts mention entity to an html anchor tag.
import { clone, htmlEscape } from './helpers'
import linkToText from './linkToText'
export default function (entity, text, options) {
const at = text.substring(entity.indices[0], entity.indices[0] + 1)
const user = htmlEscape(entity.username)
const attrs = clone(options.htmlAttrs || {})
attrs.href = options.usernameUrlBase + user
attrs.title = `@${user}`
attrs['class'] = options.usernameClass
attrs['data-username'] = user
return linkToText(entity, `${at}${user}`, attrs, options)
}

34
src/utils/linkToText.js Normal file
View File

@@ -0,0 +1,34 @@
// Returns html as text with given
import { htmlEscape, stringSupplant } from './helpers'
const BOOLEAN_ATTRIBUTES = {
disabled: true,
readonly: true,
multiple: true,
checked: true
}
function _tagAttrs (attributes) {
let htmlAttrs = ''
for (const k in attributes) {
let v = attributes[k]
if (BOOLEAN_ATTRIBUTES[k]) {
v = v ? k : null
}
if (v == null) {
continue
}
htmlAttrs += ` ${htmlEscape(k)}="${htmlEscape(v.toString())}"`
}
return htmlAttrs
}
/* eslint-disable-next-line no-unused-vars */
export default function (entity, text, attributes, options) {
const opts = {
text: text,
attr: _tagAttrs(attributes)
}
return stringSupplant('<a #{attr}>#{text}</a>', opts)
}

35
src/utils/linkToUrl.js Normal file
View File

@@ -0,0 +1,35 @@
// Converts URL entity to an html anchor tag.
import { clone, htmlEscape } from './helpers'
import linkToText from './linkToText'
const urlHasProtocol = /^https?:\/\//i
export default function (entity, text, options) {
let url = entity.url
const displayUrl = url
let linkText = htmlEscape(displayUrl)
const attrs = clone(options.htmlAttrs || {})
if (!url.match(urlHasProtocol)) {
url = `http://${url}`
}
attrs.href = url
if (options.targetBlank) {
attrs.target = '_blank'
}
// set class only if urlClass is specified.
if (options.urlClass) {
attrs['class'] = options.urlClass
}
// set target only if urlTarget is specified.
if (options.urlTarget) {
attrs.target = options.urlTarget
}
return linkToText(entity, linkText, attrs, options)
}

View File

@@ -0,0 +1,27 @@
export default function (regex, map, flags) {
flags = flags || ''
if (typeof regex !== 'string') {
if (regex.global && flags.indexOf('g') < 0) {
flags += 'g'
}
if (regex.ignoreCase && flags.indexOf('i') < 0) {
flags += 'i'
}
if (regex.multiline && flags.indexOf('m') < 0) {
flags += 'm'
}
regex = regex.source
}
return new RegExp(
regex.replace(/#\{(\w+)\}/g, function (match, name) {
let newRegex = map[name] || ''
if (typeof newRegex !== 'string') {
newRegex = newRegex.source
}
return newRegex
}),
flags
)
}

View File

@@ -0,0 +1,22 @@
import regexSupplant from './_regexSupplant'
const validCCTLD = regexSupplant(
RegExp(
'(?:(?:' +
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|' +
'ਭਾਰਤ|ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|موريتانيا|مليسيا|مصر|قطر|فلسطين|عمان|عراق|' +
'سورية|سودان|تونس|بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|' +
'мон|мкд|ею|бел|бг|ελ|zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|' +
'tt|tr|tp|to|tn|tm|tl|tk|tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|' +
'se|sd|sc|sb|sa|rw|ru|rs|ro|re|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|' +
'nl|ni|ng|nf|ne|nc|na|mz|my|mx|mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|' +
'lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|' +
'im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|' +
'fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|' +
'ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|' +
'at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
')(?=[^0-9a-zA-Z@]|$))'
)
)
export default validCCTLD

View File

@@ -0,0 +1,100 @@
import regexSupplant from './_regexSupplant'
const validGTLD = regexSupplant(
RegExp(
'(?:(?:' +
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|' +
'政府|政务|招聘|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|' +
'中文网|中信|世界|ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|' +
'كاثوليك|عرب|شبكة|بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|' +
'католик|дети|zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|' +
'yandex|yamaxun|yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|' +
'works|work|woodside|wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|' +
'weir|weibo|wedding|wed|website|weber|webcam|weatherchannel|weather|watches|watch|warman|' +
'wanggou|wang|walter|walmart|wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|' +
'vlaanderen|vivo|viva|vistaprint|vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|' +
'viajes|vet|versicherung|vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|' +
'vana|vacations|ups|uol|uno|university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|' +
'trust|travelersinsurance|travelers|travelchannel|travel|training|trading|trade|toys|toyota|' +
'town|tours|total|toshiba|toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|' +
'tiffany|tienda|tickets|tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|' +
'technology|tech|team|tdk|tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|' +
'systems|symantec|sydney|swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|' +
'sucks|style|study|studio|stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|' +
'statebank|starhub|star|staples|stada|srt|srl|spreadbetting|spot|sport|spiegel|space|soy|sony|' +
'song|solutions|solar|sohu|software|softbank|social|soccer|sncf|smile|smart|sling|skype|sky|' +
'skin|ski|site|singles|sina|silk|shriram|showtime|show|shouji|shopping|shop|shoes|shiksha|shia|' +
'shell|shaw|sharp|shangrila|sfr|sexy|sex|sew|seven|ses|services|sener|select|seek|security|' +
'secure|seat|search|scot|scor|scjohnson|science|schwarz|schule|school|scholarships|schmidt|' +
'schaeffler|scb|sca|sbs|sbi|saxo|save|sas|sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|' +
'samsclub|salon|sale|sakura|safety|safe|saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|' +
'rodeo|rocks|rocher|rmit|rip|rio|ril|rightathome|ricoh|richardli|rich|rexroth|reviews|review|' +
'restaurant|rest|republican|report|repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|' +
'redumbrella|redstone|red|recipes|realty|realtor|realestate|read|raid|radio|racing|qvc|quest|' +
'quebec|qpon|pwc|pub|prudential|pru|protection|property|properties|promo|progressive|prof|' +
'productions|prod|pro|prime|press|praxi|pramerica|post|porn|politie|poker|pohl|pnc|plus|' +
'plumbing|playstation|play|place|pizza|pioneer|pink|ping|pin|pid|pictures|pictet|pics|piaget|' +
'physio|photos|photography|photo|phone|philips|phd|pharmacy|pfizer|pet|pccw|pay|passagens|' +
'party|parts|partners|pars|paris|panerai|panasonic|pamperedchef|page|ovh|ott|otsuka|osaka|' +
'origins|orientexpress|organic|org|orange|oracle|open|ooo|onyourside|online|onl|ong|one|omega|' +
'ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|obi|nyc|ntt|nrw|nra|nowtv|nowruz|' +
'now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|nike|nico|nhk|ngo|nfl|nexus|' +
'nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|net|nec|nba|navy|natura|' +
'nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|msd|movistar|movie|mov|' +
'motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|monash|mom|moi|moe|moda|' +
'mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|miami|metlife|merckmsd|' +
'meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|mcd|mba|mattel|' +
'maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|makeup|maison|' +
'maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|lotte|' +
'london|lol|loft|locus|locker|loans|loan|llc|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|' +
'kerryhotels|kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|' +
'jewelry|jetzt|jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|' +
'ipiranga|investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|' +
'infiniti|industries|inc|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|' +
'hyundai|hyatt|hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|' +
'horse|honeywell|honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|' +
'hitachi|hisamitsu|hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|' +
'haus|hangout|hamburg|hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|' +
'gratis|graphics|grainger|gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|' +
'godaddy|gmx|gmo|gmbh|gmail|globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|' +
'genting|gent|gea|gdn|gbiz|garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|' +
'fund|fun|fujixerox|fujitsu|ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|' +
'forum|forsale|forex|ford|football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|' +
'flights|flickr|fitness|fit|fishing|fish|firmdale|firestone|fire|financial|finance|final|film|' +
'fido|fidelity|fiat|ferrero|ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|' +
'faith|fairwinds|fail|fage|extraspace|express|exposed|expert|exchange|everbank|events|eus|' +
'eurovision|etisalat|esurance|estate|esq|erni|ericsson|equipment|epson|epost|enterprises|' +
'engineering|engineer|energy|emerck|email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|' +
'dupont|duns|dunlop|duck|dubai|dtv|drive|download|dot|doosan|domains|doha|dog|dodge|doctor|' +
'docs|dnp|diy|dish|discover|discount|directory|direct|digital|diet|diamonds|dhl|dev|design|' +
'desi|dentist|dental|democrat|delta|deloitte|dell|delivery|degree|deals|dealer|deal|dds|dclk|' +
'day|datsun|dating|date|data|dance|dad|dabur|cyou|cymru|cuisinella|csc|cruises|cruise|crs|' +
'crown|cricket|creditunion|creditcard|credit|courses|coupons|coupon|country|corsica|coop|cool|' +
'cookingchannel|cooking|contractors|contact|consulting|construction|condos|comsec|computer|' +
'compare|company|community|commbank|comcast|com|cologne|college|coffee|codes|coach|clubmed|' +
'club|cloud|clothing|clinique|clinic|click|cleaning|claims|cityeats|city|citic|citi|citadel|' +
'cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|chintai|cheap|chat|chase|charity|' +
'channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|catering|cat|casino|cash|' +
'caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|capitalone|capital|' +
'capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|bzh|buzz|buy|' +
'business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|bridgestone|' +
'bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|boehringer|' +
'boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|black|biz|bio|' +
'bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|beats|bcn|bcg|' +
'bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|barclaycard|' +
'barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|autos|auto|' +
'author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|art|arpa|' +
'army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|' +
'allfinanz|alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|' +
'afl|afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|' +
'accenture|academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
')(?=[^0-9a-zA-Z@]|$))'
)
)
export default validGTLD

196
src/utils/regex/index.js Normal file
View File

@@ -0,0 +1,196 @@
import regexSupplant from './_regexSupplant'
import validCCTLD from './_validCCTLD'
import validGTLD from './_validGTLD'
function _stringSupplant (str, map) {
return str.replace(/#\{(\w+)\}/g, function (match, name) {
return map[name] || ''
})
}
/* eslint-disable no-useless-escape */
/* eslint-disable no-control-regex */
// Special types
const astralLetterAndMarks = /\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\uddfd\ude80-\ude9c\udea0-\uded0\udee0\udf00-\udf1f\udf30-\udf40\udf42-\udf49\udf50-\udf7a\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc00-\udc9d\udd00-\udd27\udd30-\udd63\ude00-\udf36\udf40-\udf55\udf60-\udf67]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37\udc38\udc3c\udc3f-\udc55\udc60-\udc76\udc80-\udc9e\udd00-\udd15\udd20-\udd39\udd80-\uddb7\uddbe\uddbf\ude00-\ude03\ude05\ude06\ude0c-\ude13\ude15-\ude17\ude19-\ude33\ude38-\ude3a\ude3f\ude60-\ude7c\ude80-\ude9c\udec0-\udec7\udec9-\udee6\udf00-\udf35\udf40-\udf55\udf60-\udf72\udf80-\udf91]|\ud803[\udc00-\udc48]|\ud804[\udc00-\udc46\udc7f-\udcba\udcd0-\udce8\udd00-\udd34\udd50-\udd73\udd76\udd80-\uddc4\uddda\ude00-\ude11\ude13-\ude37\udeb0-\udeea\udf01-\udf03\udf05-\udf0c\udf0f\udf10\udf13-\udf28\udf2a-\udf30\udf32\udf33\udf35-\udf39\udf3c-\udf44\udf47\udf48\udf4b-\udf4d\udf57\udf5d-\udf63\udf66-\udf6c\udf70-\udf74]|\ud805[\udc80-\udcc5\udcc7\udd80-\uddb5\uddb8-\uddc0\ude00-\ude40\ude44\ude80-\udeb7]|\ud806[\udca0-\udcdf\udcff\udec0-\udef8]|\ud808[\udc00-\udf98]|\ud80c[\udc00-\udfff]|\ud80d[\udc00-\udc2e]|\ud81a[\udc00-\ude38\ude40-\ude5e\uded0-\udeed\udef0-\udef4\udf00-\udf36\udf40-\udf43\udf63-\udf77\udf7d-\udf8f]|\ud81b[\udf00-\udf44\udf50-\udf7e\udf8f-\udf9f]|\ud82c[\udc00\udc01]|\ud82f[\udc00-\udc6a\udc70-\udc7c\udc80-\udc88\udc90-\udc99\udc9d\udc9e]|\ud834[\udd65-\udd69\udd6d-\udd72\udd7b-\udd82\udd85-\udd8b\uddaa-\uddad\ude42-\ude44]|\ud835[\udc00-\udc54\udc56-\udc9c\udc9e\udc9f\udca2\udca5\udca6\udca9-\udcac\udcae-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd1e-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd52-\udea5\udea8-\udec0\udec2-\udeda\udedc-\udefa\udefc-\udf14\udf16-\udf34\udf36-\udf4e\udf50-\udf6e\udf70-\udf88\udf8a-\udfa8\udfaa-\udfc2\udfc4-\udfcb]|\ud83a[\udc00-\udcc4\udcd0-\udcd6]|\ud83b[\ude00-\ude03\ude05-\ude1f\ude21\ude22\ude24\ude27\ude29-\ude32\ude34-\ude37\ude39\ude3b\ude42\ude47\ude49\ude4b\ude4d-\ude4f\ude51\ude52\ude54\ude57\ude59\ude5b\ude5d\ude5f\ude61\ude62\ude64\ude67-\ude6a\ude6c-\ude72\ude74-\ude77\ude79-\ude7c\ude7e\ude80-\ude89\ude8b-\ude9b\udea1-\udea3\udea5-\udea9\udeab-\udebb]|\ud840[\udc00-\udfff]|\ud841[\udc00-\udfff]|\ud842[\udc00-\udfff]|\ud843[\udc00-\udfff]|\ud844[\udc00-\udfff]|\ud845[\udc00-\udfff]|\ud846[\udc00-\udfff]|\ud847[\udc00-\udfff]|\ud848[\udc00-\udfff]|\ud849[\udc00-\udfff]|\ud84a[\udc00-\udfff]|\ud84b[\udc00-\udfff]|\ud84c[\udc00-\udfff]|\ud84d[\udc00-\udfff]|\ud84e[\udc00-\udfff]|\ud84f[\udc00-\udfff]|\ud850[\udc00-\udfff]|\ud851[\udc00-\udfff]|\ud852[\udc00-\udfff]|\ud853[\udc00-\udfff]|\ud854[\udc00-\udfff]|\ud855[\udc00-\udfff]|\ud856[\udc00-\udfff]|\ud857[\udc00-\udfff]|\ud858[\udc00-\udfff]|\ud859[\udc00-\udfff]|\ud85a[\udc00-\udfff]|\ud85b[\udc00-\udfff]|\ud85c[\udc00-\udfff]|\ud85d[\udc00-\udfff]|\ud85e[\udc00-\udfff]|\ud85f[\udc00-\udfff]|\ud860[\udc00-\udfff]|\ud861[\udc00-\udfff]|\ud862[\udc00-\udfff]|\ud863[\udc00-\udfff]|\ud864[\udc00-\udfff]|\ud865[\udc00-\udfff]|\ud866[\udc00-\udfff]|\ud867[\udc00-\udfff]|\ud868[\udc00-\udfff]|\ud869[\udc00-\uded6\udf00-\udfff]|\ud86a[\udc00-\udfff]|\ud86b[\udc00-\udfff]|\ud86c[\udc00-\udfff]|\ud86d[\udc00-\udf34\udf40-\udfff]|\ud86e[\udc00-\udc1d]|\ud87e[\udc00-\ude1d]|\udb40[\udd00-\uddef]/
const astralNumerals = /\ud801[\udca0-\udca9]|\ud804[\udc66-\udc6f\udcf0-\udcf9\udd36-\udd3f\uddd0-\uddd9\udef0-\udef9]|\ud805[\udcd0-\udcd9\ude50-\ude59\udec0-\udec9]|\ud806[\udce0-\udce9]|\ud81a[\ude60-\ude69\udf50-\udf59]|\ud835[\udfce-\udfff]/
const bmpLetterAndMarks = /A-Za-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u052f\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u05d0-\u05ea\u05f0-\u05f2\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07ca-\u07f5\u07fa\u0800-\u082d\u0840-\u085b\u08a0-\u08b2\u08e4-\u0963\u0971-\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7\u09c8\u09cb-\u09ce\u09d7\u09dc\u09dd\u09df-\u09e3\u09f0\u09f1\u0a01-\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a70-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0b01-\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5c\u0b5d\u0b5f-\u0b63\u0b71\u0b82\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0c00-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c58\u0c59\u0c60-\u0c63\u0c81-\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0cde\u0ce0-\u0ce3\u0cf1\u0cf2\u0d01-\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d57\u0d60-\u0d63\u0d7a-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0edc-\u0edf\u0f00\u0f18\u0f19\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u103f\u1050-\u108f\u109a-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16f1-\u16f8\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772\u1773\u1780-\u17d3\u17d7\u17dc\u17dd\u180b-\u180d\u1820-\u1877\u1880-\u18aa\u18b0-\u18f5\u1900-\u191e\u1920-\u192b\u1930-\u193b\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f\u1aa7\u1ab0-\u1abe\u1b00-\u1b4b\u1b6b-\u1b73\u1b80-\u1baf\u1bba-\u1bf3\u1c00-\u1c37\u1c4d-\u1c4f\u1c5a-\u1c7d\u1cd0-\u1cd2\u1cd4-\u1cf6\u1cf8\u1cf9\u1d00-\u1df5\u1dfc-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u20d0-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u2e2f\u3005\u3006\u302a-\u302f\u3031-\u3035\u303b\u303c\u3041-\u3096\u3099\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua672\ua674-\ua67d\ua67f-\ua69d\ua69f-\ua6e5\ua6f0\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua7ad\ua7b0\ua7b1\ua7f7-\ua827\ua840-\ua873\ua880-\ua8c4\ua8e0-\ua8f7\ua8fb\ua90a-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf\ua9e0-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa36\uaa40-\uaa4d\uaa60-\uaa76\uaa7a-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab5f\uab64\uab65\uabc0-\uabea\uabec\uabed\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf870-\uf87f\uf882\uf884-\uf89f\uf8b8\uf8c1-\uf8d6\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe00-\ufe0f\ufe20-\ufe2d\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc/
const bmpNumerals = /0-9\u0660-\u0669\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0de6-\u0def\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\ua9f0-\ua9f9\uaa50-\uaa59\uabf0-\uabf9\uff10-\uff19/
const codePoint = /(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])/
const cyrillicLettersAndMarks = /\u0400-\u04FF/
const directionalMarkersGroup = /\u202A-\u202E\u061C\u200E\u200F\u2066\u2067\u2068\u2069/
const hashtagSpecialChars = /_\u200c\u200d\ua67e\u05be\u05f3\u05f4\uff5e\u301c\u309b\u309c\u30a0\u30fb\u3003\u0f0b\u0f0c\xb7/
const invalidCharsGroup = /\uFFFE\uFEFF\uFFFF/
const latinAccentChars = /\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u024F\u0253\u0254\u0256\u0257\u0259\u025B\u0263\u0268\u026F\u0272\u0289\u028B\u02BB\u0300-\u036F\u1E00-\u1EFF/
const nonBmpCodePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/gm
const punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/
const spacesGroup = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/
const validPortNumber = /[0-9]+/
const validPunycode = /(?:xn--[\-0-9a-z]+)/
const validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i
const validUrlQueryEndingChars = /[a-z0-9\-_&=#\/]/i
// URLs
const validGeneralUrlPathChars = regexSupplant(
/[a-z#{cyrillicLettersAndMarks}0-9!*';:=+,.$/%#[\]\-\u2013_~@|&#{latinAccentChars}]/i,
{ cyrillicLettersAndMarks, latinAccentChars }
)
const validUrlBalancedParens = regexSupplant(
'\\(' +
'(?:' +
'#{validGeneralUrlPathChars}+' +
'|' +
// allow one nested level of balanced parentheses
'(?:' +
'#{validGeneralUrlPathChars}*' +
'\\(' +
'#{validGeneralUrlPathChars}+' +
'\\)' +
'#{validGeneralUrlPathChars}*' +
')' +
')' +
'\\)',
{ validGeneralUrlPathChars },
'i'
)
const validUrlPathEndingChars = regexSupplant(
/[+\-a-z#{cyrillicLettersAndMarks}0-9=_#/#{latinAccentChars}]|(?:#{validUrlBalancedParens})/i,
{ cyrillicLettersAndMarks, latinAccentChars, validUrlBalancedParens }
)
const validUrlPrecedingChars = regexSupplant(
/(?:[^A-Za-z0-9@$##{invalidCharsGroup}]|[#{directionalMarkersGroup}]|^)/,
{ invalidCharsGroup, directionalMarkersGroup }
)
const invalidDomainChars = _stringSupplant(
'#{punct}#{spacesGroup}#{invalidCharsGroup}#{directionalMarkersGroup}',
{ punct, spacesGroup, invalidCharsGroup, directionalMarkersGroup }
)
const validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/, {
invalidDomainChars
})
const validDomainName = regexSupplant(
/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/,
{ validDomainChars }
)
const validSubdomain = regexSupplant(
/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/,
{ validDomainChars }
)
const validDomain = regexSupplant(
/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/,
{ validDomainName, validSubdomain, validGTLD, validCCTLD, validPunycode }
)
const validUrlPath = regexSupplant(
'(?:' +
'(?:' +
'#{validGeneralUrlPathChars}*' +
'(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
'#{validUrlPathEndingChars}' +
')|(?:@#{validGeneralUrlPathChars}+/)' +
')',
{
validGeneralUrlPathChars,
validUrlBalancedParens,
validUrlPathEndingChars
},
'i'
)
// Hashtags
const hashtagAlpha = regexSupplant(/(?:[#{bmpLetterAndMarks}]|(?=#{nonBmpCodePairs})(?:#{astralLetterAndMarks}))/, {
bmpLetterAndMarks,
nonBmpCodePairs,
astralLetterAndMarks
})
const hashtagAlphaNumeric = regexSupplant(
/(?:[#{bmpLetterAndMarks}#{bmpNumerals}#{hashtagSpecialChars}]|(?=#{nonBmpCodePairs})(?:#{astralLetterAndMarks}|#{astralNumerals}))/,
{
bmpLetterAndMarks,
bmpNumerals,
hashtagSpecialChars,
nonBmpCodePairs,
astralLetterAndMarks,
astralNumerals
}
)
const hashtagBoundary = regexSupplant(/(?:^|\uFE0E|\uFE0F|$|(?!#{hashtagAlphaNumeric}|&)#{codePoint})/, {
codePoint,
hashtagAlphaNumeric
})
// Mentions
const validMentionPrecedingChars = /(?:^|[^a-zA-Z0-9_!#$%&*@]|(?:^|[^a-zA-Z0-9_+~.-])(?:rt|RT|rT|Rt):?)/
// =======
// Exports
// =======
// URLs
export const extractUrl = regexSupplant(
'(' + // $1 total match
'(#{validUrlPrecedingChars})' + // $2 Preceeding chracter
'(' + // $3 URL
'(https?:\\/\\/)?' + // $4 Protocol (optional)
'(#{validDomain})' + // $5 Domain(s)
'(?::(#{validPortNumber}))?' + // $6 Port number (optional)
'(\\/#{validUrlPath}*)?' + // $7 URL Path
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String
')' +
')',
{
validUrlPrecedingChars,
validDomain,
validPortNumber,
validUrlPath,
validUrlQueryChars,
validUrlQueryEndingChars
},
'gi'
)
export const validAsciiDomain = regexSupplant(
/(?:(?:[-a-z0-9#{latinAccentChars}]+)\.)+(?:#{validGTLD}|#{validCCTLD}|#{validPunycode})/gi,
{ latinAccentChars, validGTLD, validCCTLD, validPunycode }
)
export const validTcoUrl = regexSupplant(
/^https?:\/\/t\.co\/([a-z0-9]+)(?:\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?/,
{ validUrlQueryChars, validUrlQueryEndingChars },
'i'
)
// Hashtags
export const hashSigns = /[#]/
export const endHashtagMatch = regexSupplant(/^(?:#{hashSigns}|:\/\/)/, { hashSigns })
export const validHashtag = regexSupplant(
/(#{hashtagBoundary})(#{hashSigns})(?!\uFE0F|\u20E3)(#{hashtagAlphaNumeric}*#{hashtagAlpha}#{hashtagAlphaNumeric}*)/gi,
{ hashtagBoundary, hashSigns, hashtagAlphaNumeric, hashtagAlpha }
)
// Mentions
export const atSigns = /[@]/
export const endMentionMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/, { atSigns, latinAccentChars })
export const validMention = regexSupplant(
'(#{validMentionPrecedingChars})' + // $1: Preceding character
'(#{atSigns})' + // $2: At mark
'([a-zA-Z0-9_]{1,20})', // $3: Screen name
// '(/[a-zA-Z][a-zA-Z0-9_-]{0,24})?', // $4: List (optional)
{ validMentionPrecedingChars, atSigns },
'g'
)

View File

@@ -0,0 +1,15 @@
export default function (entities) {
entities.sort(function (a, b) {
return a.indices[0] - b.indices[0]
})
let prev = entities[0]
for (let i = 1; i < entities.length; i++) {
if (prev.indices[1] > entities[i].indices[0]) {
entities.splice(i, 1)
i--
} else {
prev = entities[i]
}
}
}

View File

@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -1,18 +0,0 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'home',
components: {
HelloWorld
}
}
</script>

39
vue.config.js Normal file
View File

@@ -0,0 +1,39 @@
const path = require('path')
module.exports = {
lintOnSave: false,
outputDir: './docs',
publicPath: './',
configureWebpack: {
entry: {
app: path.resolve(__dirname, 'docs-src/main.js')
},
resolve: {
extensions: [ '.styl' ],
alias: {
'@': path.resolve(__dirname, 'docs-src'),
'@vars': path.resolve(__dirname, 'docs-src/styles/variables.styl')
}
}
},
css: undefined,
chainWebpack: config => {
config.module
.rule('js')
.include
.add(path.resolve(__dirname, 'docs-src'))
config.module
.rule('vue')
.include
.add(path.resolve(__dirname, 'docs-src'))
config.module
.rule('styl')
.include
.add(path.resolve(__dirname, 'docs-src'))
}
}