├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── doc.yml │ ├── feature_request.yml │ └── question.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── gh-pages.yml │ └── prettier.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── src ├── css └── style.css ├── img └── favicon.ico ├── index.html └── js ├── accordion.js └── main.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [DenverCoder1] 2 | patreon: 3 | open_collective: 4 | ko_fi: 5 | tidelift: 6 | community_bridge: 7 | liberapay: 8 | issuehunt: 9 | otechie: 10 | custom: 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug 2 | description: Submit a bug report to help us improve 3 | title: '🐛 Bug: ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to fill out our bug report form 🙏 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: A brief description of the bug. What happened? What did you expect to happen? 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: steps 18 | attributes: 19 | label: Steps to reproduce 20 | description: How do you trigger this bug? Please walk us through it step by step. 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: screenshots 25 | attributes: 26 | label: Screenshots 27 | description: Please add screenshots if applicable 28 | validations: 29 | required: false 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/doc.yml: -------------------------------------------------------------------------------- 1 | name: 📚 Documentation 2 | description: Report an issue related to the documentation 3 | title: '📚 Docs: ' 4 | labels: ['documentation'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to make our documentation better 🙏 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: Description of the documentation issue 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Submit a proposal for a new feature or enhancement 3 | title: '🚀 Feature: ' 4 | labels: ['feature'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to fill out our feature request form 🙏 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: Description of the proposed feature or enhancement. Why should this be implemented? 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Ask a question about the project 3 | title: '❓ Question: ' 4 | labels: ['question'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to ask a question! 🙏 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: Description of the question. What would you like to know? 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # Checklist: 17 | 18 | - [ ] I have performed a self-review of my own code 19 | - [ ] I have commented my code, particularly in hard-to-understand areas 20 | - [ ] I have made corresponding changes to the documentation 21 | - [ ] My changes generate no new warnings 22 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - '.github/workflows/gh-pages.yml' 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | deploy: 17 | concurrency: ci-${{ github.ref }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Deploy 24 | uses: JamesIves/github-pages-deploy-action@v4 25 | with: 26 | branch: gh-pages 27 | folder: src 28 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Format with Prettier 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths: 9 | - "**.md" 10 | - "**.html" 11 | - "**.js" 12 | - "**.css" 13 | - ".github/workflows/prettier.yml" 14 | 15 | jobs: 16 | prettier: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Pull Request 20 | if: ${{ github.event_name == 'pull_request' }} 21 | uses: actions/checkout@v3 22 | with: 23 | repository: ${{ github.event.pull_request.head.repo.full_name }} 24 | ref: ${{ github.event.pull_request.head.ref }} 25 | 26 | - name: Checkout Push 27 | if: ${{ github.event_name != 'pull_request' }} 28 | uses: actions/checkout@v3 29 | 30 | - name: Install prettier 31 | run: npm install --global prettier 32 | 33 | - name: Check formatting with Prettier 34 | continue-on-error: true 35 | run: prettier --check "**/*.{md,css,js}"; prettier --check "**/*.html" --print-width 500 36 | 37 | - name: Prettify code 38 | run: prettier --write "**/*.{md,css,js}"; prettier --write "**/*.html" --print-width 500 39 | 40 | - name: Commit changes 41 | uses: EndBug/add-and-commit@v9 42 | with: 43 | message: "style: Formatted code with Prettier" 44 | default_author: github_actions 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode/settings.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "trailingComma": "es5", 7 | "bracketSpacing": true, 8 | "bracketSameLine": true, 9 | "printWidth": 120 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | jonah@freshidea.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing Guidelines 2 | 3 | Contributions are welcome! Feel free to open an issue or submit a pull request if you have a way to improve this project. 4 | 5 | Make sure your request is meaningful and you have tested the app locally before submitting a pull request. 6 | 7 | This documentation contains a set of guidelines to help you during the contribution process. 8 | 9 | ### Need some help regarding the basics? 10 | 11 | You can refer to the following articles on the basics of Git and GitHub in case you are stuck: 12 | 13 | - [Forking a Repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) 14 | - [Cloning a Repo](https://help.github.com/en/desktop/contributing-to-projects/creating-an-issue-or-pull-request) 15 | - [How to create a Pull Request](https://opensource.com/article/19/7/create-pull-request-github) 16 | - [Getting started with Git and GitHub](https://towardsdatascience.com/getting-started-with-git-and-github-6fcd0f2d4ac6) 17 | - [Learn GitHub from Scratch](https://lab.github.com/githubtraining/introduction-to-github) 18 | 19 | ### Clone the repository 20 | 21 | ``` 22 | git clone https://github.com/DenverCoder1/unicode-formatter.git 23 | cd unicode-formatter 24 | ``` 25 | 26 | ### Running the app locally 27 | 28 | Open `index.html` to run the project locally 29 | 30 | You may use an extension such as Live Server for VS Code to enable helpful development features. 31 | 32 | ## Formatting 33 | 34 | This project uses Prettier internally to keep the formatting consistent. 35 | 36 | To install Prettier locally, make sure you have npm installed, and run: 37 | 38 | ``` 39 | npm install prettier 40 | ``` 41 | 42 | To format the code, run: 43 | 44 | ``` 45 | prettier --write "**/*.{md,css,js}"; prettier --write "**/*.html" --print-width 500 46 | ``` 47 | 48 | Note that the `--print-width` option is used to prevent Prettier from breaking up the HTML code into multiple lines. 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DenverCoder1 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 | Format your Unicode text with 𝗯𝗼𝗹𝗱, 𝘪𝘵𝘢𝘭𝘪𝘤𝘴, and 𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 5 |
6 | and make all kinds of 𝓯𝓪𝓷𝓬𝔂 𝓽𝓮𝔁𝓽 with Unicode fonts. 7 |

8 | 9 |

10 | 11 | 12 |

13 | 14 | ## ⚡ Try it out! 15 | 16 | [unicode-formatter.demolab.com](https://unicode-formatter.demolab.com) 17 | 18 | ## ℹ️ How to use 19 | 20 | Type your text in the text area and format it by highlighting the text you want to format and clicking one of the buttons. 21 | 22 | Click "More fonts" to show more Unicode fonts. 23 | 24 | The eraser button will convert your selection back to normal text. 25 | 26 | ## 🖼️ Demo 27 | 28 | [![demo](https://user-images.githubusercontent.com/51421669/115247650-f9e60d80-a0f4-11eb-92dd-4fd060d8fd7a.gif)](https://unicode-formatter.demolab.com) 29 | 30 | ## 📜 Supported Fonts 31 | 32 | | Font | Example | 33 | | ------------------ | ------------------ | 34 | | Normal | Normal | 35 | | Sans bold | 𝗦𝗮𝗻𝘀 𝗯𝗼𝗹𝗱 | 36 | | Sans italic | 𝘚𝘢𝘯𝘴 𝘪𝘵𝘢𝘭𝘪𝘤 | 37 | | Bold italic | 𝘽𝙤𝙡𝙙 𝙞𝙩𝙖𝙡𝙞𝙘 | 38 | | Monospace | 𝙼𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 | 39 | | Fullwidth | Fullwidth | 40 | | Math Fraktur | 𝔐𝔞𝔱𝔥 𝔉𝔯𝔞𝔨𝔱𝔲𝔯 | 41 | | Math Fraktur bold | 𝕸𝖆𝖙𝖍 𝕱𝖗𝖆𝖐𝖙𝖚𝖗 𝖇𝖔𝖑𝖉 | 42 | | Serif bold | 𝐒𝐞𝐫𝐢𝐟 𝐛𝐨𝐥𝐝 | 43 | | Serif italic | 𝑆𝑒𝑟𝑖𝑓 𝑖𝑡𝑎𝑙𝑖𝑐 | 44 | | Serif bold italic | 𝑺𝒆𝒓𝒊𝒇 𝒃𝒐𝒍𝒅 𝒊𝒕𝒂𝒍𝒊𝒄 | 45 | | Math double-struck | 𝕄𝕒𝕥𝕙 𝕕𝕠𝕦𝕓𝕝𝕖-𝕤𝕥𝕣𝕦𝕔𝕜 | 46 | | Script | 𝒮𝒸𝓇𝒾𝓅𝓉 | 47 | | Bold script | 𝓑𝓸𝓵𝓭 𝓼𝓬𝓻𝓲𝓹𝓽 | 48 | | Circled | Ⓒⓘⓡⓒⓛⓔⓓ | 49 | | Circled negative | 🅒🅘🅡🅒🅛🅔🅓 🅝🅔🅖🅐🅣🅘🅥🅔 | 50 | | Squared | 🅂🅀🅄🄰🅁🄴🄳 | 51 | | Squared negative | 🆂🆀🆄🅰🆁🅴🅳 🅽🅴🅶🅰🆃🅸🆅🅴 | 52 | | Parenthesized | ⒫⒜⒭⒠⒩⒯⒣⒠⒮⒤⒵⒠⒟ | 53 | | Small Caps | Sᴍᴀʟʟ Cᴀᴩꜱ | 54 | | Subscript | ꜱᵤᵦₛ𝒸ᵣᵢₚₜ | 55 | | Superscript | ˢᵘᵖᵉʳˢᶜʳⁱᵖᵗ | 56 | | Inverted\* | Iuʌǝɹʇǝp | 57 | | Rotated Left | ᓚⴰ𝀏ơ𝀏шᓀ ⨼ш𝈯𝀏 | 58 | | Rotated Right\* | ᓓⴰ𝀏⌕𝀏ጠ௨ ᓓ𝄩மፓ𝀏 | 59 | | Mirrored\* | Miᴙᴙoᴙɘb | 60 | 61 | \* These fonts can also be transformed in Reverse (see Other Transformations below). 62 | 63 | ### Appended Styles 64 | 65 | | Font | Example | 66 | | ------------------- | ------------------- | 67 | | Underline | U͟n͟d͟e͟r͟l͟i͟n͟e͟ | 68 | | Strikethrough | 𝖲̶𝗍̶𝗋̶𝗂̶𝗄̶𝖾̶𝗍̶𝗁̶𝗋̶𝗈̶𝗎̶𝗀̶𝗁̶ | 69 | | Short Strikethrough | S̵h̵o̵r̵t̵ ̵S̵t̵r̵i̵k̵e̵t̵h̵r̵o̵u̵g̵h̵ | 70 | 71 | ## Other Transformations 72 | 73 | | Font | Example | 74 | | --------------------- | --------------------- | 75 | | Reverse | esreveR | 76 | | Inverted Reverse | ǝsɹǝʌǝꓤ pǝʇɹǝʌuI | 77 | | Rotated Right Reverse | ጠᔕᓓጠ<ጠᓓ 𝀏ፓம𝄩ᓓ ௨ጠ𝀏⌕𝀏ⴰᓓ | 78 | | Mirrored Reverse | ɘꙅᴙɘvɘЯ bɘᴙoᴙᴙiM | 79 | 80 | ## ⌨️ Keyboard shortcuts 81 | 82 | | Shortcut | Font | 83 | | ----------------------------------------------------------------------------- | ------------- | 84 | | Ctrl B | 𝗦𝗮𝗻𝘀 𝗯𝗼𝗹𝗱 | 85 | | Ctrl I | 𝘚𝘢𝘯𝘴 𝘪𝘵𝘢𝘭𝘪𝘤 | 86 | | Ctrl M | 𝙼𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 | 87 | | Ctrl U | U͟n͟d͟e͟r͟l͟i͟n͟e͟ | 88 | | Alt K or Alt Shift 5 | 𝖲̶𝗍̶𝗋̶𝗂̶𝗄̶𝖾̶𝗍̶𝗁̶𝗋̶𝗈̶𝗎̶𝗀̶𝗁̶ | 89 | | Ctrl Shift + or Ctrl . | ˢᵘᵖᵉʳˢᶜʳⁱᵖᵗ | 90 | | Ctrl = or Ctrl , | ꜱᵤᵦₛ𝒸ᵣᵢₚₜ | 91 | 92 | ## 🤗 Contributing 93 | 94 | Contributions are welcome! Feel free to open an issue or submit a pull request if you have a way to improve this project. 95 | 96 | Make sure your request is meaningful and you have tested the app locally before submitting a pull request. 97 | 98 | ## 🙋 Support 99 | 100 | 💙 If you like this project, give it a ⭐ and share it with friends! 101 | 102 |

103 | Youtube 104 | Sponsor with Github 105 |

106 | 107 | [☕ Buy me a coffee](https://ko-fi.com/jlawrence) 108 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text: black; 3 | --background: #eaeaea; 4 | --highlight: #038c78; 5 | --border: #777777; 6 | --border-hover: #333333; 7 | --textarea: #fafafa; 8 | } 9 | 10 | [data-theme="dark"] { 11 | --text: white; 12 | --background: #0d0f11; 13 | } 14 | 15 | html { 16 | -webkit-box-sizing: border-box; 17 | -moz-box-sizing: border-box; 18 | box-sizing: border-box; 19 | } 20 | 21 | *, 22 | *:before, 23 | *:after { 24 | -webkit-box-sizing: inherit; 25 | -moz-box-sizing: inherit; 26 | box-sizing: inherit; 27 | } 28 | 29 | body { 30 | background: var(--background); 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | align-items: center; 35 | height: 95vh; 36 | width: 95vw; 37 | margin: 0 auto; 38 | max-width: 600px; 39 | color: var(--text); 40 | } 41 | 42 | body, 43 | button, 44 | textarea { 45 | font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 46 | } 47 | 48 | .CodeMirror { 49 | font-family: inherit; 50 | font-size: 22px; 51 | padding: 18px; 52 | width: 100%; 53 | min-height: 300px; 54 | } 55 | 56 | button:focus, 57 | summary:focus, 58 | a:focus { 59 | outline: none; 60 | box-shadow: 0 0 0 2px var(--highlight); 61 | } 62 | 63 | /* logo */ 64 | 65 | .logo { 66 | display: flex; 67 | justify-content: center; 68 | margin: 1em auto 0.5em auto; 69 | text-decoration: none; 70 | color: var(--text); 71 | font-size: 115%; 72 | transition: color 0.2s ease-out; 73 | } 74 | 75 | .logo:hover { 76 | color: var(--highlight); 77 | } 78 | 79 | a.logo img { 80 | width: 100%; 81 | max-width: 360px; 82 | } 83 | 84 | /* controls */ 85 | 86 | .controls { 87 | padding: 0.5em; 88 | transition: all 0.4s ease-out; 89 | width: 100%; 90 | } 91 | 92 | .control-btns { 93 | display: flex; 94 | flex-wrap: wrap; 95 | margin-top: 0.5em; 96 | } 97 | 98 | .more-fonts summary { 99 | margin: 0 0 12px 6px; 100 | cursor: pointer; 101 | border-radius: 2px; 102 | } 103 | 104 | .controls button { 105 | margin: 0.4em; 106 | width: 30px; 107 | height: 30px; 108 | cursor: pointer; 109 | background: inherit; 110 | border: 1px solid var(--border); 111 | color: var(--text); 112 | border-radius: 2px; 113 | display: flex; 114 | flex-direction: column; 115 | justify-content: center; 116 | align-items: center; 117 | transition: background 0.2s ease-in; 118 | } 119 | 120 | .controls button:hover { 121 | border-color: var(--border-hover); 122 | } 123 | 124 | .more-fonts button { 125 | padding: 0px; 126 | } 127 | 128 | /* textarea */ 129 | 130 | textarea { 131 | width: 100%; 132 | height: 294px; 133 | border: none; 134 | outline: none; 135 | background: var(--textarea); 136 | padding: 1em; 137 | font-size: 1.4em; 138 | } 139 | 140 | /* github buttons */ 141 | 142 | div.github { 143 | display: flex; 144 | justify-content: center; 145 | align-items: center; 146 | flex-wrap: wrap; 147 | } 148 | 149 | .github span { 150 | margin: 0 2px; 151 | } 152 | 153 | /* buttons */ 154 | .large-buttons { 155 | display: flex; 156 | justify-content: center; 157 | align-items: center; 158 | } 159 | 160 | .btn { 161 | color: white; 162 | font-size: 1.1em; 163 | padding: 0.8em 1.1em; 164 | border-radius: 28px; 165 | display: flex; 166 | justify-content: center; 167 | align-items: center; 168 | width: 134px; 169 | text-decoration: none; 170 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 171 | transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); 172 | margin: 1.4em 0.5em 0 0.5em; 173 | border: none; 174 | outline: none; 175 | cursor: pointer; 176 | } 177 | 178 | .btn:hover { 179 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 6px 6px rgba(0, 0, 0, 0.22); 180 | } 181 | 182 | .btn svg { 183 | margin-right: 0.5em; 184 | } 185 | 186 | .tweet-button { 187 | background: #1da1f2; 188 | } 189 | 190 | .tweet-button:hover { 191 | background: #1a91da; 192 | } 193 | 194 | .copy-to-clipboard-button { 195 | background: #858585; 196 | } 197 | 198 | .copy-to-clipboard-button:hover { 199 | background: #727272; 200 | } 201 | 202 | /* tooltips */ 203 | 204 | /* bubble */ 205 | .controls button:before, 206 | .large-buttons button:before { 207 | content: attr(title); 208 | height: auto; 209 | width: auto; 210 | position: absolute; 211 | background: #4a4a4afa; 212 | border-radius: 4px; 213 | color: white; 214 | line-height: 30px; 215 | font-size: 1.1em; 216 | padding: 0 12px; 217 | transform: translate(0, -38px); 218 | transition: all 0.2s ease-out; 219 | pointer-events: none; 220 | opacity: 0; 221 | } 222 | 223 | /* triangle */ 224 | .controls button:after, 225 | .large-buttons button:after { 226 | content: ""; 227 | position: absolute; 228 | border-style: solid; 229 | border-color: #4a4a4afa transparent transparent transparent; 230 | transform: translate(0, -20px); 231 | transition: all 0.2s ease-out; 232 | pointer-events: none; 233 | opacity: 0; 234 | } 235 | 236 | .large-buttons button:before { 237 | font-size: 1em; 238 | transform: translateY(-52px); 239 | } 240 | 241 | .large-buttons button:after { 242 | transform: translateY(-34px); 243 | } 244 | 245 | .controls button:hover:before, 246 | .large-buttons button:hover:before, 247 | .controls button:hover:after, 248 | .large-buttons button:hover:after { 249 | opacity: 1; 250 | } 251 | -------------------------------------------------------------------------------- /src/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unicode-formatter/b4fbde701f0ea9bd50db85437c11bd70f5985892/src/img/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | Unicode formatter 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 |
41 | 42 | Sponsor 43 | 44 | View on GitHub 45 | 46 | Star 47 |
48 | 49 | 50 |
51 | Fonts: 52 |
53 | 59 | 60 | 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 | More fonts 70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 102 | 109 | 116 | 123 |
124 |
125 | 126 | 127 | 128 |
129 | 136 | 137 | 144 |
145 | 146 | 147 | -------------------------------------------------------------------------------- /src/js/accordion.js: -------------------------------------------------------------------------------- 1 | // Based on https://css-tricks.com/how-to-animate-the-details-element-using-waapi/ 2 | class Accordion { 3 | constructor(el) { 4 | // Store the
element 5 | this.el = el; 6 | // Store the element 7 | this.summary = el.querySelector("summary"); 8 | // Store the
element 9 | this.content = el.querySelector(".content"); 10 | // Store the animation object (so we can cancel it if needed) 11 | this.animation = null; 12 | // Store if the element is closing 13 | this.isClosing = false; 14 | // Store if the element is expanding 15 | this.isExpanding = false; 16 | // Detect user clicks on the summary element 17 | this.summary.addEventListener("click", (e) => this.onClick(e)); 18 | } 19 | 20 | onClick(e) { 21 | // Stop default behaviour from the browser 22 | e.preventDefault(); 23 | // Add an overflow on the
to avoid content overflowing 24 | this.el.style.overflow = "hidden"; 25 | // Check if the element is being closed or is already closed 26 | if (this.isClosing || !this.el.open) { 27 | this.open(); 28 | // Check if the element is being openned or is already open 29 | } else if (this.isExpanding || this.el.open) { 30 | this.shrink(); 31 | } 32 | } 33 | 34 | shrink() { 35 | // Set the element as "being closed" 36 | this.isClosing = true; 37 | // Store the current height of the element 38 | const startHeight = `${this.el.offsetHeight}px`; 39 | // Calculate the height of the summary 40 | const endHeight = `${this.summary.offsetHeight}px`; 41 | // If there is already an animation running 42 | if (this.animation) { 43 | // Cancel the current animation 44 | this.animation.cancel(); 45 | } 46 | // Start a WAAPI animation 47 | this.animation = this.el.animate( 48 | { 49 | // Set the keyframes from the startHeight to endHeight 50 | height: [startHeight, endHeight], 51 | }, 52 | { 53 | duration: 400, 54 | easing: "ease-out", 55 | } 56 | ); 57 | // When the animation is complete, call onAnimationFinish() 58 | this.animation.onfinish = () => this.onAnimationFinish(false); 59 | // If the animation is cancelled, isClosing variable is set to false 60 | this.animation.oncancel = () => (this.isClosing = false); 61 | } 62 | 63 | open() { 64 | // Apply a fixed height on the element 65 | this.el.style.height = `${this.el.offsetHeight}px`; 66 | // Force the [open] attribute on the details element 67 | this.el.open = true; 68 | // Wait for the next frame to call the expand function 69 | window.requestAnimationFrame(() => this.expand()); 70 | } 71 | 72 | expand() { 73 | // Set the element as "being expanding" 74 | this.isExpanding = true; 75 | // Get the current fixed height of the element 76 | const startHeight = `${this.el.offsetHeight}px`; 77 | // Calculate the open height of the element (summary height + content height) 78 | const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`; 79 | // If there is already an animation running 80 | if (this.animation) { 81 | // Cancel the current animation 82 | this.animation.cancel(); 83 | } 84 | // Start a WAAPI animation 85 | this.animation = this.el.animate( 86 | { 87 | // Set the keyframes from the startHeight to endHeight 88 | height: [startHeight, endHeight], 89 | }, 90 | { 91 | duration: 400, 92 | easing: "ease-out", 93 | } 94 | ); 95 | // When the animation is complete, call onAnimationFinish() 96 | this.animation.onfinish = () => this.onAnimationFinish(true); 97 | // If the animation is cancelled, isExpanding variable is set to false 98 | this.animation.oncancel = () => (this.isExpanding = false); 99 | } 100 | 101 | onAnimationFinish(open) { 102 | // Set the open attribute based on the parameter 103 | this.el.open = open; 104 | // Clear the stored animation 105 | this.animation = null; 106 | // Reset isClosing & isExpanding 107 | this.isClosing = false; 108 | this.isExpanding = false; 109 | // Remove the overflow hidden and the fixed height 110 | this.el.style.height = this.el.style.overflow = ""; 111 | } 112 | } 113 | 114 | document.querySelectorAll("details").forEach((el) => { 115 | new Accordion(el); 116 | }); 117 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | let formatter = { 2 | // prettier-ignore 3 | fonts: { 4 | normal: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~", 5 | sans: "\"\\ !#$%&'()*+,-./𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫:;<=>?@𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹[]^_`𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓{|}~", 6 | sansBold: "\"\\ !#$%&'()*+,-./𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵:;<=>?@𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭[]^_`𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇{|}~", 7 | sansItalic: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡[]^_`𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻{|}~", 8 | sansBoldItalic: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕[]^_`𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯{|}~", 9 | monospace: "\"\\ !#$%&'()*+,-./𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿:;<=>?@𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉[]^_`𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣{|}~", 10 | fullwidth: "\"\ !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~", 11 | fraktur: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ[]^_`𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷{|}~", 12 | boldFraktur: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝕬𝕭𝕮𝕯𝕰𝕱𝕲𝕳𝕴𝕵𝕶𝕷𝕸𝕹𝕺𝕻𝕼𝕽𝕾𝕿𝖀𝖁𝖂𝖃𝖄𝖅[]^_`𝖆𝖇𝖈𝖉𝖊𝖋𝖌𝖍𝖎𝖏𝖐𝖑𝖒𝖓𝖔𝖕𝖖𝖗𝖘𝖙𝖚𝖛𝖜𝖝𝖞𝖟{|}~", 13 | serifBold: "\"\\ !#$%&'()*+,-./𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗:;<=>?@𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙[]^_`𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳{|}~", 14 | serifItalic: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍[]^_`𝑎𝑏𝑐𝑑𝑒𝑓𝑔ℎ𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧{|}~", 15 | serifBoldItalic: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁[]^_`𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛{|}~", 16 | doubleStruck: "\"\\ !#$%&'()*+,-./𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡:;<=>?@𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ[]^_`𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫{|}~", 17 | script: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵[]^_`𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏{|}~", 18 | boldScript: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩[]^_`𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃{|}~", 19 | circled: "\"⦸ !#$%&'()⊛⊕,⊖⨀⊘⓪①②③④⑤⑥⑦⑧⑨:;⧀⊜⧁?@ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ[]^_`ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ{⦶}~", 20 | circledNegative: "\"\\ !#$%&'()*+,-./⓿❶❷❸❹❺❻❼❽❾:;<=>?@🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩[]^_`🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩{|}~", 21 | squared: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉[]^_`🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉{|}~", 22 | squaredNegative: "\"⧅ !#$%&'()⧆⊞,⊟⊡⧄0123456789:;<=>?@🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉[]^_`🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉{|}~", 23 | parenthesized: "\"\\ !#$%&'()*+,-./0⑴⑵⑶⑷⑸⑹⑺⑻⑼:;<=>?@⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵[]^_`⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵{|}~", 24 | smallCaps: "\"\\ !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴩꞯʀꜱᴛᴜᴠᴡxʏᴢ{|}~", 25 | subscript: "\"\\ !#$%&'₍₎*₊,₋./₀₁₂₃₄₅₆₇₈₉:;<₌>?@ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘ🇶ʀꜱᴛᴜᴠᴡxʏᴢ[]^_`ₐᵦ𝒸𝒹ₑ𝒻𝓰ₕᵢⱼₖₗₘₙₒₚᵩᵣₛₜᵤᵥ𝓌ₓᵧ𝓏{|}~", 26 | superscript: "\"\\ !#$%&'⁽⁾*⁺,⁻./⁰¹²³⁴⁵⁶⁷⁸⁹:;<⁼>?@ᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᵠᴿˢᵀᵁⱽᵂˣʸᶻ[]^_`ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖᵠʳˢᵗᵘᵛʷˣʸᶻ{|}~", 27 | inverted: "„\\ ¡#$%⅋,)(*+‘-˙/0ƖՇƐᔭϛ9𝘓86:;<=>¿@∀ꓭↃꓷƎℲ⅁HIſꓘ⅂WNOԀῸꓤS⊥∩ꓥMX⅄Z][^‾`ɐqɔpǝɟƃɥıɾʞןɯuodbɹsʇnʌʍxʎz}|{~", 28 | mirrored: "\"/ !#$%&')(*+,-.\\0߁ςƐ߂टმ٢8୧:;<=>⸮@AꓭↃꓷƎꟻӘHIႱꓘ⅃MИOꟼϘЯꙄTUVWXYZ][^_`ɒdↄbɘʇϱʜiįʞlmᴎoqpᴙꙅɈυvwxγz}|{~", 29 | rotatedLeft: "=/ !#$%&-⏝⏜*+`ǀ∙\\ⴰ↽വ𝈐ፓහமΓꝏᓂ⠒;˅𝄥∧ᣇ@ᗉߘ𝈱⌓ш𝈯ᘎ⌶𝄩⥟𝈎⨼∑Zⴰᓇⵚᓚᔕ⊢⊃𝈷ᕒ×⤚𝇙⎵⎴‹|`ơᓄ𝈱ᓀш𝈯თ𝈦𝄩ᓜ𝈎⨼ᗴ⊂ⴰᓇᓂᓚᔕ𝀏⊃𝈷З×⤚𝇙⏟_⏞ಽ", 30 | rotatedRight: "=/ !#$%&-⏜⏝*+`ǀ∙\\ⴰ⇀ᘚω𝈦හの⨼ꝏᓄ⠒;∧𝄥˅?@ᗆϖᴒᗜጠ╖ᘏ⌶𝄩ᓚ⌤⌐ᕒZⴰᓀᓄᓓᔕ⊣⊂<ᓬ×⤙𝇙⎴⎵›|`⌕ᓂᴒ௨ጠ╖மፓ𝄩ᓚ⌤⌐ᴟᴝⴰᓀᓄᓓᔕ𝀏⊂<ᓬ×⤙𝇙⏞_⏟ಽ", 31 | }, 32 | 33 | // initialize formatter with CodeMirror 34 | init: function (textarea) { 35 | // no code highlighting and wrap long lines 36 | this.CodeMirror = CodeMirror.fromTextArea(textarea, { 37 | mode: null, 38 | lineWrapping: true, 39 | }); 40 | 41 | // list of font characters for checking if character is formatted 42 | this.allCharacters = new Set(Object.values(this.fonts).join("")); 43 | 44 | // mapping functions 45 | const bold = () => this.formatSelections("sansBold"); 46 | const italic = () => this.formatSelections("sansItalic"); 47 | const monospace = () => this.formatSelections("monospace"); 48 | const strikethrough = () => 49 | this.formatSelections("", { 50 | append: "̶", 51 | }); 52 | const underline = () => 53 | this.formatSelections("", { 54 | append: "͟", 55 | }); 56 | const superscript = () => this.formatSelections("superscript"); 57 | const subscript = () => this.formatSelections("subscript"); 58 | 59 | // add keymaps 60 | this.CodeMirror.setOption("extraKeys", { 61 | "Ctrl-B": bold, 62 | "Ctrl-I": italic, 63 | "Ctrl-M": monospace, 64 | "Ctrl-U": underline, 65 | "Alt-K": strikethrough, 66 | "Shift-Alt-5": strikethrough, 67 | "Shift-Ctrl-=": superscript, 68 | "Ctrl-.": superscript, 69 | "Ctrl-=": subscript, 70 | "Ctrl-,": subscript, 71 | }); 72 | }, 73 | 74 | // check if text is already formatted with a certain font 75 | alreadyFormatted: function (text, font) { 76 | const fontCharacters = new Set(this.fonts[font]); 77 | // flag as already formatted if all characters are in font or not in any other font 78 | return Array.from(text).every((char) => fontCharacters.has(char) || !this.allCharacters.has(char)); 79 | }, 80 | 81 | // check if text is already formatted with a certain font 82 | alreadyAppended: function (text, append) { 83 | // check if at least half the characters are the append character 84 | return Array.from(text).filter((char) => char == append).length >= text.length / 2; 85 | }, 86 | 87 | // format text into selected font 88 | formatText: function (text, font, options) { 89 | // set font to normal if already formatted with selected font 90 | if (this.fonts[font] && this.alreadyFormatted(text, font)) { 91 | font = "normal"; 92 | } 93 | // remove and don't append if character is already appended 94 | if (options?.append) { 95 | options.remove = options.append; 96 | options.append = !this.alreadyAppended(text, options.append) ? options.append : ""; 97 | } 98 | // Array.from() splits the string by symbol and not by code points 99 | let newText = Array.from(text); 100 | // exchange font symbols 101 | if (this.fonts[font]) { 102 | const targetFont = Array.from(this.fonts[font]); 103 | const charLists = Object.values(this.fonts); 104 | // map characters to new font 105 | newText = newText.map((char) => { 106 | let index; 107 | // find the index of the character in some font 108 | const found = charLists.some((charList) => { 109 | index = Array.from(charList).indexOf(char); 110 | return index > -1; 111 | }); 112 | // if found, replace with the corresponding character in the target font 113 | // if not found, keep the character the same 114 | return found ? targetFont[index] : char; 115 | }); 116 | } 117 | // reverse text if reverse option is set 118 | newText = options?.reverse ? newText.reverse() : newText; 119 | // remove appended symbol of specific type from the end 120 | newText = options?.remove 121 | ? newText.map((char) => char.replace(new RegExp(options.remove + "$", "u"), "")) 122 | : newText; 123 | // append symbol (underline, strikethrough, etc.) to end of each character if append is set 124 | newText = options?.append ? newText.map((char) => char + options.append) : newText; 125 | // remove appended symbols (underline, strikethrough, etc.) if using eraser 126 | // \u035f = Underline, \u0333 = Double Underline, \u0335 = Short Strikethrough \u0336 = Strikethrough 127 | newText = options?.clear ? newText.map((char) => char.replace(/\u035f|\u0333|\u0335|\u0336/gu, "")) : newText; 128 | // set textarea content and select text around the replacement 129 | return newText.join(""); 130 | }, 131 | 132 | // format selected text 133 | formatSelections: function (font, options) { 134 | // for each selection (there can be multiple), format the text 135 | const newTexts = this.CodeMirror.getSelections().map((selection) => this.formatText(selection, font, options)); 136 | // replace all selections with replacements 137 | this.CodeMirror.replaceSelections(newTexts, "around"); 138 | }, 139 | 140 | // open twitter with the text value as the post 141 | tweet: function () { 142 | const text = this.CodeMirror.getValue(); 143 | const encoded = encodeURIComponent(text); 144 | const twitterUrl = `https://twitter.com/intent/tweet?text=${encoded}`; 145 | const win = window.open(twitterUrl, "_blank"); 146 | win.focus(); 147 | }, 148 | 149 | // copy the text to the clipboard 150 | copy: function (el) { 151 | // create dummy textarea with text content 152 | const textarea = document.createElement("textarea"); 153 | textarea.value = this.CodeMirror.getValue(); 154 | document.body.appendChild(textarea); 155 | // select all 156 | textarea.select(); 157 | textarea.setSelectionRange(0, 99999); 158 | // copy 159 | document.execCommand("copy"); 160 | // remove textarea 161 | textarea.parentElement.removeChild(textarea); 162 | // set tooltip text 163 | el.title = "Copied!"; 164 | }, 165 | }; 166 | 167 | let tooltip = { 168 | // put the original title back (eg. "Copied!" => "Copy to clipboard") 169 | resetTooltip: function (el) { 170 | el.title = el.dataset.originalTitle; 171 | }, 172 | }; 173 | 174 | // when the page loads 175 | window.addEventListener( 176 | "load", 177 | function () { 178 | // textarea for initializing CodeMirror 179 | const textarea = document.querySelector("textarea"); 180 | // initialize formatter 181 | formatter.init(textarea); 182 | // add click event listeners to format buttons 183 | document.querySelectorAll(".control-btns button").forEach(function (btn) { 184 | btn.addEventListener( 185 | "click", 186 | function () { 187 | // format highlighted text into selected font 188 | formatter.formatSelections(this.className, { ...this.dataset }); 189 | }, 190 | false 191 | ); 192 | }); 193 | // set dark mode on preference 194 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 195 | document.body.setAttribute("data-theme", "dark"); 196 | } 197 | }, 198 | false 199 | ); 200 | --------------------------------------------------------------------------------