├── .github
└── FUNDING.yml
├── .gitignore
├── .npmrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── index.html
├── netlify.toml
├── package.json
├── pnpm-lock.yaml
├── public
├── favicon.png
├── favicon.svg
├── pwa-192x192.png
├── pwa-512x512.png
├── robots.txt
└── safari-pinned-tab.svg
├── src
├── App.vue
├── components
│ ├── Editor.vue
│ ├── ErrorPopup.vue
│ ├── NavBar.vue
│ ├── ResultArea.vue
│ └── SearchArea.vue
├── logics
│ ├── dark.ts
│ ├── index.ts
│ └── store.ts
├── main.ts
├── modes
│ └── regex.ts
├── pages
│ ├── [...all].vue
│ └── index.vue
├── shims.d.ts
└── styles
│ └── main.css
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: antfu
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vite-ssg-dist
3 | .vite-ssg-temp
4 | *.local
5 | dist
6 | dist-ssr
7 | node_modules
8 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "johnsoncodehk.volar",
4 | "lokalise.i18n-ally",
5 | "antfu.iconify",
6 | "dbaeumer.vscode-eslint"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "volar.tsPlugin": true,
3 | "i18n-ally.localesPaths": "locales",
4 | "i18n-ally.keystyle": "nested",
5 | "i18n-ally.sortKeys": true,
6 | "cSpell.words": [
7 | "Vitesse"
8 | ],
9 | "typescript.tsdk": "node_modules/typescript/lib",
10 | "volar.tsPluginStatus": false,
11 | "svg.preview.background": "transparent"
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Anthony Fu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ReX
5 |
6 | Transform texts with RegExp like a Pro.
7 |
8 |
9 | Go to App
10 |
11 |
12 | ## Motivation
13 |
14 | Sometimes you need to do some clean up or transform for texts in batches. You can either edit manually line-by-line, using multi-cursors in your editor, apply find & replace with RegExp, or write a script for that.
15 |
16 | Editing manually sometime could be laborious, writing a script could be complicated and overkill. Replacing via RegExp sounds like a good options to me, but somehow most editors can only do replacement but lack of the feature "keep only what matched".
17 |
18 | Introducing **ReX**, an tiny app for me to do the text transformation without causing my headache. It was wrote in like 4 hours, but I will try to add more features as needed along the way.
19 |
20 | So, yeah, enjoy. I would be glad if you find it useful as well.
21 |
22 |
23 | ## Sponsors
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## License
32 |
33 | MIT License © 2021 [Anthony Fu](https://github.com/antfu)
34 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build.environment]
2 | NPM_FLAGS = "--prefix=/dev/null"
3 | NODE_VERSION = "14"
4 |
5 | [build]
6 | publish = "dist"
7 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build"
8 |
9 | [[redirects]]
10 | from = "/*"
11 | to = "/index.html"
12 | status = 200
13 |
14 | [[headers]]
15 | for = "/manifest.webmanifest"
16 | [headers.values]
17 | Content-Type = "application/manifest+json"
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "vite --port 3333 --open",
5 | "build": "cross-env NODE_ENV=production vite build"
6 | },
7 | "dependencies": {
8 | "@vueuse/core": "^4.2.2",
9 | "@vueuse/head": "^0.3.1",
10 | "codemirror": "^5.59.4",
11 | "vue": "^3.0.6",
12 | "vue-router": "^4.0.4"
13 | },
14 | "devDependencies": {
15 | "@antfu/eslint-config": "^0.4.3",
16 | "@iconify/json": "^1.1.308",
17 | "@types/codemirror": "^0.0.108",
18 | "@typescript-eslint/eslint-plugin": "^4.15.2",
19 | "@vitejs/plugin-vue": "^1.1.4",
20 | "@vue/compiler-sfc": "^3.0.6",
21 | "@vue/server-renderer": "^3.0.6",
22 | "@vueuse/motion": "^1.1.0",
23 | "cross-env": "^7.0.3",
24 | "eslint": "^7.20.0",
25 | "pnpm": "^5.18.1",
26 | "typescript": "^4.2.2",
27 | "vite": "^2.0.3",
28 | "vite-plugin-components": "^0.7.3",
29 | "vite-plugin-icons": "^0.2.4",
30 | "vite-plugin-pages": "^0.4.5",
31 | "vite-plugin-pwa": "^0.5.5",
32 | "vite-plugin-windicss": "^0.5.2",
33 | "vite-ssg": "^0.8.11"
34 | },
35 | "eslintConfig": {
36 | "extends": "@antfu/eslint-config",
37 | "rules": {
38 | "no-unused-vars": "off",
39 | "@typescript-eslint/no-unused-vars": "off"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antfu/rex/68cff77ed58d85abedb6c66400a7dff80d5b7175/public/favicon.png
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/pwa-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antfu/rex/68cff77ed58d85abedb6c66400a7dff80d5b7175/public/pwa-192x192.png
--------------------------------------------------------------------------------
/public/pwa-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antfu/rex/68cff77ed58d85abedb6c66400a7dff80d5b7175/public/pwa-512x512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
13 |
18 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
112 |
--------------------------------------------------------------------------------
/src/components/ErrorPopup.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 | {{ error }}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/NavBar.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Re X
9 |
10 |
11 |
16 |
17 |
18 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/ResultArea.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | TEXT
16 |
17 |
18 |
19 |
20 |
21 |
content = ''"
25 | >
26 |
27 |
28 |
34 |
35 |
36 |
copy(content)">
37 |
38 |
39 |
40 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | {{ mode.toUpperCase() }}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
70 |
71 |
72 |
78 |
79 |
80 |
86 |
87 |
88 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
109 |
110 |
111 |
127 |
128 |
129 |
130 |
131 |
132 |
137 |
--------------------------------------------------------------------------------
/src/components/SearchArea.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
24 |
25 |
motions.replaceBox.leave(done)"
28 | >
29 |
64 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/logics/dark.ts:
--------------------------------------------------------------------------------
1 | import { useDark, useToggle } from '@vueuse/core'
2 |
3 | export const isDark = useDark()
4 | export const toggleDark = useToggle(isDark)
5 |
--------------------------------------------------------------------------------
/src/logics/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dark'
2 | export * from './store'
3 |
--------------------------------------------------------------------------------
/src/logics/store.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue'
2 | import { useDebounce, useLocalStorage, useRefHistory } from '@vueuse/core'
3 | import DEFAULT_CONTENT from '../../README.md?raw'
4 |
5 | const DEFAULT_FIND = '(<.*>)(.*)()'
6 | const DEFAULT_MODE = 'take'
7 |
8 | export const findRaw = useLocalStorage('find', DEFAULT_FIND)
9 | export const flags = useLocalStorage('flags', 'gm')
10 | export const replaceRaw = useLocalStorage('replace', '')
11 | export const content = useLocalStorage('content', DEFAULT_CONTENT)
12 | export const mode = useLocalStorage<'replace' | 'take'>('mode', DEFAULT_MODE)
13 | export const takeJoint = useLocalStorage('take-join', '\n')
14 | export const takeGroup = useLocalStorage('take-group', 0)
15 | export const linewrap = useLocalStorage('linewrap', true)
16 | export const history = useRefHistory(content, { clone: false })
17 |
18 | export const error = ref(null)
19 |
20 | export const throttleFind = useDebounce(findRaw, 300)
21 | export const findRegex = computed(() => {
22 | error.value = null
23 | try {
24 | return new RegExp(throttleFind.value, flags.value)
25 | }
26 | catch (e) {
27 | error.value = e
28 | console.error(e)
29 | return new RegExp('', 'g')
30 | }
31 | })
32 |
33 | export function replace() {
34 | content.value = content.value.replace(findRegex.value, replaceRaw.value)
35 | }
36 |
37 | export function cleanup() {
38 | flags.value = 'gm'
39 | findRaw.value = ''
40 | replaceRaw.value = ''
41 | }
42 |
43 | function take(iterable: IterableIterator, length: number): T[] {
44 | return Array.from((function* () {
45 | const iterator = iterable[Symbol.iterator]()
46 | while (length-- > 0)
47 | yield iterator.next().value
48 | })())
49 | }
50 |
51 | export const matchResult = computed(() => {
52 | return take(content.value.matchAll(findRegex.value), 30)
53 | .filter(i => i)
54 | .map(i => i[0])
55 | .join('\n')
56 | })
57 |
58 | export const matches = computed(() => Array.from(content.value.matchAll(findRegex.value)))
59 |
60 | export const fullResult = computed(() => {
61 | if (mode.value === 'replace') {
62 | return content.value.replaceAll(findRegex.value, replaceRaw.value)
63 | }
64 | else if (mode.value === 'take') {
65 | return Array.from(content.value.matchAll(findRegex.value))
66 | .map(i => i?.[takeGroup.value])
67 | .filter(i => i != null)
68 | .join(takeJoint.value)
69 | }
70 | return ''
71 | })
72 |
73 | export function toggleMode() {
74 | if (mode.value === 'replace')
75 | mode.value = 'take'
76 | else
77 | mode.value = 'replace'
78 | }
79 |
80 | export function applyResult() {
81 | content.value = fullResult.value
82 | }
83 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import 'windi.css'
2 | import './styles/main.css'
3 | import { ViteSSG } from 'vite-ssg'
4 | import routes from 'pages-generated'
5 | import { MotionPlugin } from '@vueuse/motion'
6 | import App from './App.vue'
7 |
8 | // https://github.com/antfu/vite-ssg
9 | export const createApp = ViteSSG(
10 | App,
11 | { routes },
12 | ({ app }) => {
13 | app.use(MotionPlugin)
14 | },
15 | )
16 |
--------------------------------------------------------------------------------
/src/modes/regex.ts:
--------------------------------------------------------------------------------
1 | import CodeMirror from 'codemirror'
2 | import 'codemirror/addon/mode/simple'
3 |
4 | CodeMirror.defineSimpleMode('regex', {
5 | start: [
6 | { regex: /\(.*\)/, token: 'atom' },
7 | { regex: /\[.*\]/, token: 'string' },
8 | { regex: /\\\\/, token: 'builtin' },
9 | { regex: /\\\w/, token: 'keyword' },
10 | { regex: /[+\-?]/, token: 'operator' },
11 | ],
12 | })
13 |
--------------------------------------------------------------------------------
/src/pages/[...all].vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Not Found
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/shims.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | // extend the window
3 | }
4 |
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --fg: #222;
3 | --fg-semi: #2225;
4 | }
5 |
6 | html,
7 | body,
8 | #app {
9 | height: 100vh;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | html.dark {
15 | background: #222;
16 | color: #ddd;
17 | --fg: #ddd;
18 | --fg-semi: #ddd5;
19 | }
20 |
21 | .gray-border {
22 | @apply border border-gray-200 dark:border-gray-600 outline-none;
23 | }
24 |
25 | .input {
26 | @apply outline-none border border-transparent focus-within:border-orange-400;
27 | @apply bg-trueGray-50 hover:bg-trueGray-100;
28 | @apply dark:(bg-trueGray-700 hover:bg-trueGray-600);
29 | }
30 |
31 |
32 | .icon {
33 | @apply opacity-50 px-2 rounded h-8.5 mr-2 inline-block align-middle transition-all duration-200;
34 | outline: none !important;
35 | }
36 | .icon:not(.static) {
37 | @apply hover:(!opacity-100 text-orange-500) pointer-cursor;
38 | }
39 | .icon:not(.relax) {
40 | @apply w-8.5;
41 | }
42 | button.icon.active {
43 | @apply bg-orange-300 bg-opacity-10 text-orange-500 !opacity-75;
44 | }
45 |
46 | [disabled] {
47 | @apply !pointer-events-none !opacity-10;
48 | }
49 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite-plugin-windicss'
2 |
3 | export default defineConfig({
4 | darkMode: 'class',
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | mono: 'Input Mono, Fira-Code, monospace',
9 | },
10 | },
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "ESNext",
5 | "target": "es2016",
6 | "lib": ["DOM", "ESNext"],
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "incremental": true,
10 | "skipLibCheck": true,
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | "noUnusedLocals": true,
14 | "strictNullChecks": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "types": [
17 | "vite/client",
18 | "vite-plugin-pages/client"
19 | ],
20 | "paths": {
21 | "~/*": ["src/*"]
22 | }
23 | },
24 | "exclude": ["dist", "node_modules"]
25 | }
26 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { defineConfig } from 'vite'
3 | import Vue from '@vitejs/plugin-vue'
4 | import Pages from 'vite-plugin-pages'
5 | import ViteIcons, { ViteIconsResolver } from 'vite-plugin-icons'
6 | import ViteComponents from 'vite-plugin-components'
7 | import WindiCSS from 'vite-plugin-windicss'
8 | import { VitePWA } from 'vite-plugin-pwa'
9 |
10 | export default defineConfig({
11 | resolve: {
12 | alias: {
13 | '~/': `${path.resolve(__dirname, 'src')}/`,
14 | },
15 | },
16 | plugins: [
17 | Vue(),
18 | Pages(),
19 | ViteComponents({
20 | customComponentResolvers: [
21 | ViteIconsResolver({
22 | componentPrefix: '',
23 | }),
24 | ],
25 | }),
26 | ViteIcons(),
27 | WindiCSS(),
28 | VitePWA({
29 | manifest: {
30 | name: 'ReX',
31 | short_name: 'ReX',
32 | theme_color: '#ffffff',
33 | icons: [
34 | {
35 | src: '/pwa-192x192.png',
36 | sizes: '192x192',
37 | type: 'image/png',
38 | },
39 | {
40 | src: '/pwa-512x512.png',
41 | sizes: '512x512',
42 | type: 'image/png',
43 | },
44 | {
45 | src: '/pwa-512x512.png',
46 | sizes: '512x512',
47 | type: 'image/png',
48 | purpose: 'any maskable',
49 | },
50 | ],
51 | },
52 | }),
53 | ],
54 | optimizeDeps: {
55 | include: [
56 | 'vue',
57 | 'vue-router',
58 | '@vueuse/core',
59 | ],
60 | exclude: [
61 | 'vue-demi',
62 | ],
63 | },
64 | })
65 |
--------------------------------------------------------------------------------