├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── name.svg ├── package.json ├── prettier.config.js ├── public ├── 404.html ├── CNAME ├── favicon.png ├── flag.svg ├── index.html ├── logo.png └── manifest.json ├── src ├── assets │ └── logo.svg ├── components │ ├── button │ │ ├── Button.module.css │ │ ├── Button.tsx │ │ └── index.ts │ ├── card │ │ ├── Card.module.css │ │ ├── Card.tsx │ │ └── index.ts │ ├── github │ │ ├── GitHubLogo.tsx │ │ └── index.ts │ └── preview-item │ │ ├── PreviewItem.module.css │ │ ├── PreviewItem.tsx │ │ └── index.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── utils │ ├── converter.test.ts │ ├── converter.ts │ └── decorate.ts └── views │ ├── App.module.css │ ├── App.test.tsx │ ├── App.tsx │ ├── home │ ├── Home.module.css │ ├── Home.tsx │ └── index.ts │ ├── reactive-editor │ ├── ReactiveEditor.module.css │ ├── ReactiveEditor.tsx │ └── index.ts │ ├── select-decorator │ ├── SelectDecorator.module.css │ ├── SelectDecorator.tsx │ └── index.ts │ └── select-mapper │ ├── SelectMapper.module.css │ ├── SelectMapper.tsx │ └── index.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/** -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Web", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}/web/src", 10 | "sourceMapPathOverrides": { 11 | "webpack:///src/*": "${webRoot}/*" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | { "language": "typescript", "autoFix": true }, 7 | { "language": "typescriptreact", "autoFix": true } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hardo 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 | Transform text into mathematical alphanumeric 𝗌𝗒𝗆𝖻𝗈𝗅𝗌. 4 | 5 | 6 | -------------------------------------------------------------------------------- /name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symbols", 3 | "version": "0.1.0", 4 | "homepage": "https://symbols.hardo.me", 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject", 10 | "lint": "prettier --list-different \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", 11 | "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", 12 | "predeploy": "npm run build", 13 | "deploy": "gh-pages -d build" 14 | }, 15 | "dependencies": { 16 | "classnames": "^2.2.6", 17 | "react": "^16.8.6", 18 | "react-clipboard.js": "^2.0.7", 19 | "react-dom": "^16.8.6", 20 | "react-router-dom": "^5.0.0" 21 | }, 22 | "devDependencies": { 23 | "@types/classnames": "^2.2.7", 24 | "@types/jest": "24.0.13", 25 | "@types/node": "12.0.2", 26 | "@types/react": "16.8.18", 27 | "@types/react-dom": "16.8.4", 28 | "@types/react-router-dom": "^4.3.3", 29 | "gh-pages": "^2.0.1", 30 | "husky": "^2.3.0", 31 | "lint-staged": "^8.1.7", 32 | "prettier": "^1.17.1", 33 | "react-scripts": "3.0.1", 34 | "typescript": "3.4.5" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "husky": { 52 | "hooks": { 53 | "pre-commit": "lint-staged" 54 | } 55 | }, 56 | "lint-staged": { 57 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 58 | "prettier --write", 59 | "git add" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | }; 10 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | symbols.hardo.me -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noobnooc/symbols/09080fc9a5deca477156cfb13ecb9d6ccb32e984/public/favicon.png -------------------------------------------------------------------------------- /public/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | 23 | 𝒮𝓎𝓂𝒷ℴ𝓁𝓈 - Transform text into mathematical alphanumeric 𝗌𝗒𝗆𝖻𝗈𝗅𝗌. 24 | 25 | 26 | 27 | 65 | 66 | 67 | 68 |
69 |
72 | the People's Republic of China flag 77 | 84 |
85 |
86 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noobnooc/symbols/09080fc9a5deca477156cfb13ecb9d6ccb32e984/public/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "𝒮𝓎𝓂𝒷ℴ𝓁𝓈", 3 | "name": "Transform text into mathematical alphanumeric 𝗌𝗒𝗆𝖻𝗈𝗅𝗌.", 4 | "icons": [ 5 | { 6 | "src": "logo.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#ff4757", 14 | "background_color": "#dfe4ea" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/button/Button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | color: var(--color-text); 3 | background-color: var(--color-secondary); 4 | padding: var(--dimen-medium) var(--dimen-large); 5 | border-radius: var(--dimen-small); 6 | box-shadow: var(--shadow-light); 7 | border: 0; 8 | outline: none; 9 | cursor: pointer; 10 | transition: background-color 0.3s, box-shadow 0.3s; 11 | } 12 | 13 | .button:hover { 14 | background-color: var(--color-secondary-dark); 15 | box-shadow: var(--shadow-dark); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, MouseEvent } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './Button.module.css'; 5 | 6 | export type ButtonProps = { 7 | className?: string; 8 | onClick?(event: MouseEvent): void; 9 | }; 10 | 11 | export const Button: FC = ({ className, children, onClick }) => { 12 | return ( 13 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /src/components/card/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | box-shadow: var(--shadow-light); 3 | border-radius: var(--radius-medium); 4 | padding: var(--dimen-medium); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './Card.module.css'; 5 | 6 | export type CardProps = { 7 | className?: string; 8 | }; 9 | 10 | export const Card: FC = ({ className, children }) => { 11 | return
{children}
; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Card'; 2 | -------------------------------------------------------------------------------- /src/components/github/GitHubLogo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | export type GitHubLogoProps = { 4 | className?: string; 5 | }; 6 | 7 | export const GitHubLogo: FC = ({ className }) => { 8 | return ( 9 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/github/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GitHubLogo'; 2 | -------------------------------------------------------------------------------- /src/components/preview-item/PreviewItem.module.css: -------------------------------------------------------------------------------- 1 | .previewItem { 2 | padding: var(--dimen-medium) var(--dimen-large); 3 | border-radius: var(--dimen-small); 4 | border-left: solid 3px var(--color-primary); 5 | box-shadow: var(--shadow-light); 6 | cursor: pointer; 7 | transition: box-shadow 0.3s; 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | } 12 | 13 | .previewItem:hover { 14 | box-shadow: var(--shadow-dark); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/preview-item/PreviewItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, MouseEvent } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './PreviewItem.module.css'; 5 | 6 | export type PreviewItemProps = { 7 | className?: string; 8 | onClick?(event: MouseEvent): void; 9 | }; 10 | 11 | export const PreviewItem: FC = ({ 12 | className, 13 | children, 14 | onClick, 15 | }) => { 16 | return ( 17 |
21 | {children} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/preview-item/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PreviewItem'; 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary: #ff4757; 3 | --color-primary-dark: #c5002e; 4 | --color-primary-light: #ff7d84; 5 | --color-secondary: #70a1ff; 6 | --color-secondary-dark: #3473cb; 7 | --color-secondary-light: #a6d2ff; 8 | --color-text: #ffffff; 9 | --color-icon: #ffffff; 10 | --color-text-primary: #212121; 11 | --color-text-secondary: #757575; 12 | --color-divider: #bdbdbd; 13 | --color-error: #c5002e; 14 | --color-success: #2ed573; 15 | --color-info: #1e90ff; 16 | --color-warning: #ffa502; 17 | --color-white: #ffffff; 18 | --color-black: #000000; 19 | 20 | --shadow-dark: 0 2px 4px 0 rgba(47, 53, 66, 0.1), 21 | 0 0 10px 0 rgba(87, 96, 111, 0.3); 22 | --shadow-light: 0 2px 4px 0 rgba(47, 53, 66, 0.1), 23 | 0 0 10px 0 rgba(87, 96, 111, 0.15); 24 | 25 | --radius-huge: 20px; 26 | --radius-large: 10px; 27 | --radius-medium: 5px; 28 | --radius-small: 2px; 29 | 30 | --dimen-huge: 50px; 31 | --dimen-large: 20px; 32 | --dimen-medium: 10px; 33 | --dimen-small: 5px; 34 | } 35 | 36 | * { 37 | box-sizing: border-box; 38 | } 39 | 40 | html, 41 | body, 42 | #root { 43 | height: 100%; 44 | } 45 | 46 | body { 47 | margin: 0; 48 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 49 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 50 | sans-serif, PingFangSC-Light, 'Microsoft YaHei UI', 'Microsoft YaHei'; 51 | -webkit-font-smoothing: antialiased; 52 | -moz-osx-font-smoothing: grayscale; 53 | color: var(--color-text-primary); 54 | } 55 | 56 | code { 57 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 58 | monospace; 59 | } 60 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from 'views/App'; 5 | 6 | import 'index.css'; 7 | import * as serviceWorker from 'serviceWorker'; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: https://bit.ly/CRA-PWA 14 | serviceWorker.register(); 15 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 20 | ), 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href, 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA', 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.', 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/utils/converter.test.ts: -------------------------------------------------------------------------------- 1 | import { SymbolMapperType, convertToSymbols } from './converter'; 2 | 3 | const TEST_TEXT = 4 | 'If you shed tears when you miss the sun, you also miss the stars.'; 5 | 6 | const TEST_CASE_MAP = new Map([ 7 | [ 8 | 'circled', 9 | 'Ⓘⓕ ⓨⓞⓤ ⓢⓗⓔⓓ ⓣⓔⓐⓡⓢ ⓦⓗⓔⓝ ⓨⓞⓤ ⓜⓘⓢⓢ ⓣⓗⓔ ⓢⓤⓝ, ⓨⓞⓤ ⓐⓛⓢⓞ ⓜⓘⓢⓢ ⓣⓗⓔ ⓢⓣⓐⓡⓢ.', 10 | ], 11 | [ 12 | 'double-struck', 13 | '𝕀𝕗 𝕪𝕠𝕦 𝕤𝕙𝕖𝕕 𝕥𝕖𝕒𝕣𝕤 𝕨𝕙𝕖𝕟 𝕪𝕠𝕦 𝕞𝕚𝕤𝕤 𝕥𝕙𝕖 𝕤𝕦𝕟, 𝕪𝕠𝕦 𝕒𝕝𝕤𝕠 𝕞𝕚𝕤𝕤 𝕥𝕙𝕖 𝕤𝕥𝕒𝕣𝕤.', 14 | ], 15 | [ 16 | 'full-width', 17 | 'If you shed tears when you miss the sun, you also miss the stars.', 18 | ], 19 | [ 20 | 'mono-space', 21 | '𝙸𝚏 𝚢𝚘𝚞 𝚜𝚑𝚎𝚍 𝚝𝚎𝚊𝚛𝚜 𝚠𝚑𝚎𝚗 𝚢𝚘𝚞 𝚖𝚒𝚜𝚜 𝚝𝚑𝚎 𝚜𝚞𝚗, 𝚢𝚘𝚞 𝚊𝚕𝚜𝚘 𝚖𝚒𝚜𝚜 𝚝𝚑𝚎 𝚜𝚝𝚊𝚛𝚜.', 22 | ], 23 | [ 24 | 'fraktur', 25 | 'ℑ𝔣 𝔶𝔬𝔲 𝔰𝔥𝔢𝔡 𝔱𝔢𝔞𝔯𝔰 𝔴𝔥𝔢𝔫 𝔶𝔬𝔲 𝔪𝔦𝔰𝔰 𝔱𝔥𝔢 𝔰𝔲𝔫, 𝔶𝔬𝔲 𝔞𝔩𝔰𝔬 𝔪𝔦𝔰𝔰 𝔱𝔥𝔢 𝔰𝔱𝔞𝔯𝔰.', 26 | ], 27 | [ 28 | 'script', 29 | 'ℐ𝒻 𝓎ℴ𝓊 𝓈𝒽ℯ𝒹 𝓉ℯ𝒶𝓇𝓈 𝓌𝒽ℯ𝓃 𝓎ℴ𝓊 𝓂𝒾𝓈𝓈 𝓉𝒽ℯ 𝓈𝓊𝓃, 𝓎ℴ𝓊 𝒶𝓁𝓈ℴ 𝓂𝒾𝓈𝓈 𝓉𝒽ℯ 𝓈𝓉𝒶𝓇𝓈.', 30 | ], 31 | ]); 32 | 33 | it('convert text to symbols', () => { 34 | for (let [type, result] of TEST_CASE_MAP) { 35 | let convertedText = convertToSymbols(TEST_TEXT, type); 36 | 37 | expect(convertedText).toBe(result); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/utils/converter.ts: -------------------------------------------------------------------------------- 1 | const UPPER_CASE_RANGE_START = 'A'.charCodeAt(0); 2 | const UPPER_CASE_RANGE_END = 'Z'.charCodeAt(0); 3 | 4 | const LOWER_CASE_RANGE_START = 'a'.charCodeAt(0); 5 | const LOWER_CASE_RANGE_END = 'z'.charCodeAt(0); 6 | 7 | const NUMBER_RANGE_START = '0'.charCodeAt(0); 8 | const NUMBER_RANGE_END = '9'.charCodeAt(0); 9 | 10 | export type SymbolMapperType = 11 | | 'normal' 12 | | 'circled' 13 | | 'circled-negative' 14 | | 'squared' 15 | | 'squared-negative' 16 | | 'full-width' 17 | | 'script' 18 | | 'script-bold' 19 | | 'fraktur' 20 | | 'fraktur-bold' 21 | | 'double-struck' 22 | | 'mono-space' 23 | | 'sans-serif' 24 | | 'sans-serif-bold' 25 | | 'sans-serif-italic' 26 | | 'sans-serif-bold-italic' 27 | | 'serif-bold' 28 | | 'serif-italic' 29 | | 'serif-bold-italic' 30 | | 'capitalized'; 31 | 32 | export const symbolMapperMap: Map = new Map([ 33 | ['normal', ''], 34 | ['circled', 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'], 35 | ['circled-negative', '🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩'], 36 | ['squared', '🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉'], 37 | ['squared-negative', '🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉'], 38 | [ 39 | 'full-width', 40 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 41 | ], 42 | ['capitalized', 'ABCDEFGHIJKLMNOPQRSTUVWXYZᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘqʀꜱᴛᴜᴠᴡxʏᴢ'], 43 | ['script', '𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏'], 44 | ['script-bold', '𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃'], 45 | ['fraktur', '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'], 46 | ['fraktur-bold', '𝕬𝕭𝕮𝕯𝕰𝕱𝕲𝕳𝕴𝕵𝕶𝕷𝕸𝕹𝕺𝕻𝕼𝕽𝕾𝕿𝖀𝖁𝖂𝖃𝖄𝖅𝖆𝖇𝖈𝖉𝖊𝖋𝖌𝖍𝖎𝖏𝖐𝖑𝖒𝖓𝖔𝖕𝖖𝖗𝖘𝖙𝖚𝖛𝖜𝖝𝖞𝖟'], 47 | [ 48 | 'double-struck', 49 | '𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡', 50 | ], 51 | [ 52 | 'mono-space', 53 | '𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', 54 | ], 55 | [ 56 | 'sans-serif', 57 | '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', 58 | ], 59 | [ 60 | 'sans-serif-bold', 61 | '𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵', 62 | ], 63 | ['sans-serif-italic', '𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻'], 64 | [ 65 | 'sans-serif-bold-italic', 66 | '𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯', 67 | ], 68 | [ 69 | 'serif-bold', 70 | '𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗', 71 | ], 72 | ['serif-italic', '𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍𝑎𝑏𝑐𝑑𝑒𝑓𝑔ℎ𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧'], 73 | ['serif-bold-italic', '𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛'], 74 | ]); 75 | 76 | export function getSymbolMapperTypes(): SymbolMapperType[] { 77 | return Array.from(symbolMapperMap.keys()); 78 | } 79 | 80 | export function convertToSymbols(text: string, type: SymbolMapperType) { 81 | let mapper = symbolMapperMap.get(type); 82 | 83 | if (!mapper) { 84 | return text; 85 | } 86 | 87 | return convertToSymbolsByMapper(text, mapper); 88 | } 89 | 90 | function convertToSymbolsByMapper(text: string, mapper: string): string { 91 | return text 92 | .split('') 93 | .map(char => { 94 | let index = getMapperIndex(char); 95 | 96 | return index === -1 ? char : Array.from(mapper)[index]; 97 | }) 98 | .join(''); 99 | } 100 | 101 | function getMapperIndex(char: string): number { 102 | let charCode = char.charCodeAt(0); 103 | 104 | if (charCode >= UPPER_CASE_RANGE_START && charCode <= UPPER_CASE_RANGE_END) { 105 | return charCode - UPPER_CASE_RANGE_START; 106 | } else if ( 107 | charCode >= LOWER_CASE_RANGE_START && 108 | charCode <= LOWER_CASE_RANGE_END 109 | ) { 110 | return charCode - LOWER_CASE_RANGE_START + 26; 111 | } else if (charCode >= NUMBER_RANGE_START && charCode <= NUMBER_RANGE_END) { 112 | return charCode - NUMBER_RANGE_START + 26 + 26; 113 | } 114 | 115 | return -1; 116 | } 117 | -------------------------------------------------------------------------------- /src/utils/decorate.ts: -------------------------------------------------------------------------------- 1 | export function decorate(text: string, decorator: number) { 2 | if (!decorator || !Number.isInteger(decorator)) { 3 | return text; 4 | } 5 | 6 | return Array.from(text) 7 | .map(char => char + String.fromCodePoint(decorator)) 8 | .join(''); 9 | } 10 | 11 | export function getDecorators(): number[] { 12 | return [0, ...range(768, 879)]; 13 | } 14 | 15 | function range(start: number, end: number): number[] { 16 | const length = end - start + 1; 17 | return Array.from({ length }, (_, i) => start + i); 18 | } 19 | -------------------------------------------------------------------------------- /src/views/App.module.css: -------------------------------------------------------------------------------- 1 | .app { 2 | min-height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .header { 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | 12 | .logo { 13 | width: 150px; 14 | padding: var(--dimen-small) var(--dimen-medium); 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .logo img { 20 | width: 100%; 21 | } 22 | 23 | .onGitHub { 24 | align-self: flex-start; 25 | margin: var(--dimen-medium); 26 | padding: var(--dimen-small) var(--dimen-medium); 27 | border-radius: var(--dimen-small); 28 | box-shadow: var(--shadow-light); 29 | background-color: var(--color-secondary); 30 | color: var(--color-text); 31 | display: flex; 32 | align-items: center; 33 | text-decoration: none; 34 | cursor: pointer; 35 | } 36 | 37 | .gitHubLogo { 38 | margin-right: var(--dimen-small); 39 | fill: var(--color-icon); 40 | } 41 | 42 | .main { 43 | flex: 1; 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | .footer { 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: flex-end; 52 | align-items: center; 53 | color: var(--color-text-secondary); 54 | padding: var(--dimen-medium); 55 | text-decoration: none; 56 | } 57 | 58 | .footer a, 59 | .footer a:visited { 60 | color: var(--color-text-secondary); 61 | text-decoration: none; 62 | } 63 | 64 | .footer a:hover { 65 | text-decoration: underline; 66 | } 67 | -------------------------------------------------------------------------------- /src/views/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/views/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; 3 | 4 | import logo from 'assets/logo.svg'; 5 | import { GitHubLogo } from 'components/github'; 6 | 7 | import styles from './App.module.css'; 8 | import { Home } from './home'; 9 | import { SelectMapper } from './select-mapper'; 10 | import { ReactiveEditor } from './reactive-editor'; 11 | import { SelectDecorator } from './select-decorator'; 12 | 13 | const App: FC = () => { 14 | return ( 15 | 16 |
17 |
18 | 19 | Logo 20 | 21 | 27 | 28 | Source On GitHub 29 | 30 |
31 |
32 | 33 | 34 | 35 | 39 | 43 | 44 | 45 |
46 | 55 |
56 |
57 | ); 58 | }; 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /src/views/home/Home.module.css: -------------------------------------------------------------------------------- 1 | .home { 2 | flex: 1; 3 | display: flex; 4 | height: 100%; 5 | flex-direction: column; 6 | justify-content: center; 7 | width: 100%; 8 | max-width: 500px; 9 | align-self: center; 10 | } 11 | 12 | .mainCard { 13 | width: 100%; 14 | max-width: calc(100% - 50px); 15 | margin: 0 25px; 16 | transition: box-shadow 0.3s; 17 | display: flex; 18 | flex-direction: column; 19 | align-self: center; 20 | background-color: var(--color-primary); 21 | } 22 | 23 | .mainCard:hover { 24 | box-shadow: var(--shadow-dark); 25 | } 26 | 27 | .logo { 28 | align-self: flex-end; 29 | font-size: 2em; 30 | color: var(--color-text); 31 | margin-bottom: var(--dimen-medium); 32 | } 33 | 34 | .mainCardTextArea { 35 | resize: vertical; 36 | min-height: 200px; 37 | border: 0; 38 | border-radius: var(--radius-medium); 39 | outline: none; 40 | padding: var(--dimen-medium); 41 | font-size: 1.1em; 42 | } 43 | 44 | .transformLink { 45 | margin: 25px; 46 | } 47 | 48 | .transformButton { 49 | width: 100%; 50 | padding: var(--dimen-large); 51 | font-size: 1.3em; 52 | } 53 | -------------------------------------------------------------------------------- /src/views/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, 3 | useState, 4 | useCallback, 5 | ChangeEvent, 6 | MouseEvent, 7 | } from 'react'; 8 | import classNames from 'classnames'; 9 | 10 | import { Card } from 'components/card'; 11 | 12 | import styles from './Home.module.css'; 13 | import { Button } from 'components/button'; 14 | import { Link } from 'react-router-dom'; 15 | 16 | export const Home: FC = () => { 17 | let [text, setText] = useState(''); 18 | 19 | let onTextAreaChange = useCallback( 20 | ({ currentTarget: { value } }: ChangeEvent) => { 21 | setText(value); 22 | }, 23 | [], 24 | ); 25 | 26 | let onLinkClick = useCallback( 27 | (event: MouseEvent) => { 28 | if (!text) { 29 | event.preventDefault(); 30 | } 31 | }, 32 | [text], 33 | ); 34 | 35 | return ( 36 |
37 | 38 |
𝒮𝓎𝓂𝒷ℴ𝓁𝓈
39 |