This commit is contained in:
sharevb 2024-06-09 01:56:56 -07:00 committed by GitHub
commit 1ba0201315
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 403 additions and 8 deletions

11
components.d.ts vendored
View File

@ -127,24 +127,18 @@ declare module '@vue/runtime-core' {
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCode: typeof import('naive-ui')['NCode']
NButton: typeof import('naive-ui')['NButton']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLabel: typeof import('naive-ui')['NLabel']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpin: typeof import('naive-ui')['NSpin']
NSpace: typeof import('naive-ui')['NSpace']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
@ -179,6 +173,7 @@ declare module '@vue/runtime-core' {
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
UnicodeFormatter: typeof import('./src/tools/unicode-formatter/unicode-formatter.vue')['default']
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']

View File

@ -6,6 +6,7 @@ import { tool as asciiTextDrawer } from './ascii-text-drawer';
import { tool as textToUnicode } from './text-to-unicode';
import { tool as safelinkDecoder } from './safelink-decoder';
import { tool as unicodeFormatter } from './unicode-formatter';
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
import { tool as numeronymGenerator } from './numeronym-generator';
import { tool as macAddressGenerator } from './mac-address-generator';
@ -172,6 +173,7 @@ export const toolsByCategory: ToolCategory[] = [
textDiff,
numeronymGenerator,
asciiTextDrawer,
unicodeFormatter,
],
},
{

View File

@ -0,0 +1,12 @@
import { Edit } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Unicode Formatter',
path: '/unicode-formatter',
description: 'Format text using Unicode fonts',
keywords: ['unicode', 'formatter', 'fonts'],
component: () => import('./unicode-formatter.vue'),
icon: Edit,
createdAt: new Date('2024-04-07'),
});

View File

@ -0,0 +1,29 @@
{
"normal": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",
"sans": "\"\\ !#$%&'()*+,-./𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫:;<=>?@𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹[]^_`𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓{|}~",
"sansBold": "\"\\ !#$%&'()*+,-./𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵:;<=>?@𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭[]^_`𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇{|}~",
"sansItalic": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡[]^_`𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻{|}~",
"sansBoldItalic": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕[]^_`𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯{|}~",
"monospace": "\"\\!#$%&'()*+,-./𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿:;<=>?@𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉[]^_`𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣{|}~",
"fullwidth": "\" <>_",
"fraktur": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝔄𝔅𝔇𝔈𝔉𝔊𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔𝔖𝔗𝔘𝔙𝔚𝔛𝔜[]^_`𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷{|}~",
"boldFraktur": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝕬𝕭𝕮𝕯𝕰𝕱𝕲𝕳𝕴𝕵𝕶𝕷𝕸𝕹𝕺𝕻𝕼𝕽𝕾𝕿𝖀𝖁𝖂𝖃𝖄𝖅[]^_`𝖆𝖇𝖈𝖉𝖊𝖋𝖌𝖍𝖎𝖏𝖐𝖑𝖒𝖓𝖔𝖕𝖖𝖗𝖘𝖙𝖚𝖛𝖜𝖝𝖞𝖟{|}~",
"serifBold": "\"\\ !#$%&'()*+,-./𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗:;<=>?@𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙[]^_`𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳{|}~",
"serifItalic": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍[]^_`𝑎𝑏𝑐𝑑𝑒𝑓𝑔ℎ𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧{|}~",
"serifBoldItalic": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁[]^_`𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛{|}~",
"doubleStruck": "\"\\ !#$%&'()*+,-./𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡:;<=>?@𝔸𝔹𝔻𝔼𝔽𝔾𝕀𝕁𝕂𝕃𝕄𝕆𝕊𝕋𝕌𝕍𝕎𝕏𝕐[]^_`𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫{|}~",
"script": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝒜𝒞𝒟𝒢𝒥𝒦𝒩𝒪𝒫𝒬𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵[]^_`𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏{|}~",
"boldScript": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩[]^_`𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃{|}~",
"circled": "\"⦸ !#$%&'()⊛⊕,⊖⨀⊘⓪①②③④⑤⑥⑦⑧⑨:;⧀⊜⧁?@ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ[]^_`ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ{⦶}~",
"circledNegative": "\"\\ !#$%&'()*+,-./⓿❶❷❸❹❺❻❼❽❾:;<=>?@🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩[]^_`🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩{|}~",
"squared": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉[]^_`🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉{|}~",
"squaredNegative": "\"⧅ !#$%&'()⧆⊞,⊟⊡⧄0123456789:;<=>?@🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉[]^_`🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉{|}~",
"parenthesized": "\"\\ !#$%&'()*+,-./0⑴⑵⑶⑷⑸⑹⑺⑻⑼:;<=>?@⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵[]^_`⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵{|}~",
"smallCaps": "\"\\ !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`ᴀʙᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴩꞯʀxʏ{|}~",
"subscript": "\"\\ !#$%&'₍₎*₊,₋./₀₁₂₃₄₅₆₇₈₉:;<₌>?@ᴀʙᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴘ🇶ʀxʏ[]^_`ₐᵦ𝒸𝒹ₑ𝒻𝓰ₕᵢⱼₖₗₘₙₒₚᵩᵣₛₜᵤᵥ𝓌ₓᵧ𝓏{|}~",
"superscript": "\"\\ !#$%&'⁽⁾*⁺,⁻./⁰¹²³⁴⁵⁶⁷⁸⁹:;<⁼>?@ᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᵠᴿˢᵀᵁⱽᵂˣʸᶻ[]^_`ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖᵠʳˢᵗᵘᵛʷˣʸᶻ{|}~",
"inverted": "„\\ ¡#$%⅋,)(*+-˙/0ƖՇƐᔭϛ9𝘓86:;<=>¿@∀ꓭↃꓷƎℲ⅁HIſꓘ⅂WNOԀῸꓤS⊥∩ꓥMX⅄Z][^‾`ɐqɔpǝɟƃɥıɾʞןɯuodbɹsʇnʌʍxʎz}|{~",
"mirrored": "\"/ !#$%&')(*+,-.\\0߁ςƐ߂टმ٢8୧:;<=>⸮@AꓭↃꓷƎꟻӘHIႱꓘ⅃MИOꟼϘЯTUVWXYZ][^_`ɒdↄbɘʇϱʜiįʞlmᴎoqpᴙꙅɈυvwxγz}|{~",
"rotatedLeft": "=/ !#$%&-⏝⏜*+`ǀ∙\\ⴰ↽വ𝈐ፓහமΓꝏᓂ⠒;˅𝄥∧ᣇ@ᗉߘ𝈱⌓ш𝈯ᘎ⌶𝄩⥟𝈎⨼∑Zⴰᓇⵚᓚᔕ⊢⊃𝈷×⤚𝇙⎵⎴|`ơᓄ𝈱ᓀш𝈯თ𝈦𝄩ᓜ𝈎⨼ᗴ⊂ⴰᓇᓂᓚᔕ𝀏⊃𝈷З×⤚𝇙⏟_⏞ಽ",
"rotatedRight": "=/ !#$%&-⏜⏝*+`ǀ∙\\ⴰ⇀ᘚω𝈦හの⨼ꝏᓄ⠒;∧𝄥˅?@ᗆϖᴒᗜጠ╖ᘏ⌶𝄩ᓚ⌤⌐ᕒZⴰᓀᓄᓓᔕ⊣⊂<ᓬ×⤙𝇙⎴⎵›|`⌕ᓂᴒ௨ጠ╖மፓ𝄩ᓚ⌤⌐ᴟᴝⴰᓀᓄᓓᔕ𝀏⊂<ᓬ×⤙𝇙⏞_⏟ಽ"
}

View File

@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import { formatTextPart } from './unicode-formatter.service';
describe('unicode-formatter', () => {
it('formatTextPart applies correct style/font on text part', async () => {
expect(formatTextPart('The quick fox', 4, 9, 'sansBold')).to.deep.equal({
from: 4,
text: 'The 𝗾𝘂𝗶𝗰𝗸 fox',
to: 14,
});
expect(formatTextPart('The quick fox', 4, 13, 'boldScript')).to.deep.equal({
from: 4,
text: 'The 𝓺𝓾𝓲𝓬𝓴 𝓯𝓸𝔁',
to: 21,
});
expect(formatTextPart('The quick fox', 4, 6, 'smallCaps')).to.deep.equal({
from: 4,
text: 'The ꞯick fox',
to: 6,
});
expect(formatTextPart('The quick fox', 4, 8, 'inverted')).to.deep.equal({
from: 4,
text: 'The bnıɔk fox',
to: 8,
});
expect(formatTextPart('The quick fox', 4, 10, 'mirrored')).to.deep.equal({
from: 4,
text: 'The pυiↄʞ fox',
to: 10,
});
expect(formatTextPart('The quick fox', 4, 11, 'serifBoldItalic')).to.deep.equal({
from: 4,
text: 'The 𝒒𝒖𝒊𝒄𝒌 𝒇ox',
to: 17,
});
});
it('formatTextPart un-applies correct style/font on text part', async () => {
expect(formatTextPart('The 𝗾𝘂𝗶𝗰𝗸 fox', 4, 14, 'sansBold')).to.deep.equal({
from: 4,
text: 'The quick fox',
to: 9,
});
expect(formatTextPart('The 𝓺𝓾𝓲𝓬𝓴 𝓯𝓸𝔁', 4, 18, 'boldScript')).to.deep.equal({
from: 4,
text: 'The quick f𝓸𝔁',
to: 12,
});
expect(formatTextPart('The 𝓺𝓾𝓲𝓬𝓴 𝓯𝓸𝔁', 4, 14, 'boldScript')).to.deep.equal({
from: 4,
text: 'The quick 𝓯𝓸𝔁',
to: 9,
});
expect(formatTextPart('The 𝓺𝓾𝓲𝓬𝓴 fox', 4, 15, 'boldScript')).to.deep.equal({
from: 4,
text: 'The quick fox',
to: 10,
});
expect(formatTextPart('The pυiↄʞ fox', 4, 10, 'mirrored')).to.deep.equal({
from: 4,
text: 'The puick fox', // can't be fully restored
to: 10,
});
expect(formatTextPart('The bnıɔk fox', 4, 10, 'inverted')).to.deep.equal({
from: 4,
text: 'The quıɔʞ fox',
to: 10,
});
expect(formatTextPart('The 𝒒𝒖𝒊𝒄𝒌 𝒇ox', 4, 17, 'serifBoldItalic')).to.deep.equal({
from: 4,
text: 'The quick fox',
to: 11,
});
});
});

View File

@ -0,0 +1,103 @@
// prettier-ignore
import fonts from './unicode-fonts.json';
export type AllFontNames = keyof typeof fonts;
export interface AllOptions {
remove?: string
append?: string
reverse?: boolean
clear?: boolean
};
// list of font characters for checking if character is formatted
const allCharacters = new Set(Object.values(fonts).join(''));
// check if text is already formatted with a certain font
function alreadyFormatted(text: string, font: AllFontNames) {
const fontCharacters = new Set(fonts[font]);
// flag as already formatted if all characters are in font or not in any other font
return Array.from(text).every(char => fontCharacters.has(char) || !allCharacters.has(char));
}
// check if text is already formatted with a certain font
function alreadyAppended(text: string, append: string) {
// check if at least half the characters are the append character
return Array.from(text).filter(char => char === append).length >= text.length / 2;
}
// format text into selected font
function formatText(text: string, font: AllFontNames | undefined, options?: AllOptions) {
// set font to normal if already formatted with selected font
if (font && fonts[font] && alreadyFormatted(text, font)) {
font = 'normal';
}
// remove and don't append if character is already appended
if (options?.append) {
options.remove = options.append;
options.append = !alreadyAppended(text, options.append) ? options.append : '';
}
// Array.from() splits the string by symbol and not by code points
let newText = Array.from(text);
// exchange font symbols
if (font && fonts[font]) {
const targetFont = Array.from(fonts[font]);
const charLists = Object.values(fonts);
// map characters to new font
newText = newText.map((char) => {
let index = -1;
// find the index of the character in some font
const found = charLists.some((charList) => {
index = Array.from(charList).indexOf(char);
return index > -1;
});
// if found, replace with the corresponding character in the target font
// if not found, keep the character the same
return found ? targetFont[index] : char;
});
}
// reverse text if reverse option is set
newText = options?.reverse ? newText.reverse() : newText;
// remove appended symbol of specific type from the end
newText = options?.remove
? newText.map(char => char.replace(new RegExp(`${options.remove}$`, 'u'), ''))
: newText;
// append symbol (underline, strikethrough, etc.) to end of each character if append is set
newText = options?.append ? newText.map(char => char + options.append) : newText;
// remove appended symbols (underline, strikethrough, etc.) if using eraser
// \u035f = Underline, \u0333 = Double Underline, \u0335 = Short Strikethrough \u0336 = Strikethrough
newText = options?.clear ? newText.map(char => char.replace(/\u035F|\u0333|\u0335|\u0336/gu, '')) : newText;
// set textarea content and select text around the replacement
return newText.join('');
}
export function formatTextPart(
text: string,
selectionStart: number, selectionEnd: number,
font: AllFontNames | undefined,
options?: AllOptions) {
const regexSpaces = /^(\s*)(.+?)(\s*)$/g; // NOSONAR
const [_, spaceBefore, selection, spaceAfter] = regexSpaces.exec(text.substring(selectionStart, selectionEnd) || '') || [];
const prefix = text.substring(0, selectionStart);
const newSelection = formatText(selection, font, options);
const suffix = text.substring(selectionEnd);
return {
text: `${prefix}${spaceBefore}${newSelection}${spaceAfter}${suffix}`,
from: selectionStart,
to: selectionStart + (spaceBefore?.length || 0) + newSelection.length + (spaceAfter?.length || 0),
};
}
// format selected text
export function formatSelection(textArea: HTMLTextAreaElement, font: AllFontNames | undefined, options?: AllOptions) {
const selectionStart = textArea.selectionStart;
const selectionEnd = textArea.selectionEnd;
const oldText = textArea.value;
const { text: newText, from, to } = formatTextPart(oldText, selectionStart, selectionEnd, font, options);
textArea.value = newText;
textArea.setSelectionRange(from, to);
textArea.focus();
}

View File

@ -0,0 +1,179 @@
<script lang="ts" setup>
// import {AllFontNames,AllOptions, bold,italic,monospace,strikethrough,underline,superscript,subscript,formatSelections} from './unicod'
import { Copy } from '@vicons/tabler';
import type { AllFontNames, AllOptions } from './unicode-formatter.service';
import { formatSelection } from './unicode-formatter.service';
import { useCopy } from '@/composable/copy';
const textArea = ref<HTMLTextAreaElement>();
function formatTextAreaSelection(format: AllFontNames | undefined, options: AllOptions) {
if (!textArea.value) {
return;
}
formatSelection(textArea.value, format, options);
}
const formattedText = ref('Type your text here and 𝗳𝗼𝗿𝗺𝗮𝘁 it by highlighting the text you want to format and clicking one of the buttons above. Click "More fonts" to show more Unicode fonts. The eraser button will convert your selection back to normal text.');
const { copy: copyFormattedText } = useCopy({ source: formattedText, text: 'Formatted text copied to the clipboard' });
</script>
<template>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" rel="stylesheet">
<div
max-w-600
style="font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;"
>
<!-- First set of buttons -->
<n-form-item label="Fonts:" mb-3 mt-3 w-full>
<n-button title="Normal" @click="formatTextAreaSelection('normal', { clear: true })">
<!-- Eraser Icon -->
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M2.586,15.408l4.299,4.299C7.072,19.895,7.326,20,7.592,20h0.001h2h0.4h9.6v-2h-6.958l7.222-7.222 c0.78-0.779,0.78-2.049,0-2.828L14.906,3c-0.779-0.779-2.049-0.779-2.828,0l-4.75,4.749l-4.754,4.843 C1.809,13.371,1.813,14.634,2.586,15.408z M13.492,4.414l4.95,4.95l-2.586,2.586L10.906,7L13.492,4.414z M8.749,9.156l0.743-0.742 l4.95,4.95l-4.557,4.557C9.86,17.946,9.838,17.973,9.816,18H9.593H8.006l-4.005-4.007L8.749,9.156z" />
</svg>
</n-button>
<n-button title="Bold" @click="formatTextAreaSelection('sansBold', { })">
𝗕
</n-button>
<n-button title="Italic" @click="formatTextAreaSelection('sansItalic', { })">
𝘐
</n-button>
<n-button title="Monospace" @click="formatTextAreaSelection('monospace', { })">
𝙼
</n-button>
<n-button style="text-decoration: underline" @click="formatTextAreaSelection(undefined, { append: '͟' })">
U
</n-button>
<n-button style="text-decoration: line-through" @click="formatTextAreaSelection(undefined, { append: '̶' })">
S
</n-button>
</n-form-item>
<!-- Collapsible section with more buttons -->
<details style="display:block" borders mb-3 w-full>
<summary>More fonts</summary>
<n-space mt-3 w-full>
<n-button title="Math Sans" @click="formatTextAreaSelection('sans', { })">
𝖬
</n-button>
<n-button title="Sans Bold italic" @click="formatTextAreaSelection('sansBoldItalic', { })">
𝘽𝙄
</n-button>
<n-button title="Serif bold" @click="formatTextAreaSelection('serifBold', { })">
𝐁
</n-button>
<n-button title="Serif italic" @click="formatTextAreaSelection('serifItalic', { })">
𝐼
</n-button>
<n-button title="Serif bold italic" @click="formatTextAreaSelection('serifBoldItalic', { })">
𝑩𝑰
</n-button>
<n-button title="Double Underline" @click="formatTextAreaSelection(undefined, { append: '̳' })">
U̳
</n-button>
<n-button title="Short Strikethrough" style="text-decoration: line-through" @click="formatTextAreaSelection(undefined, { append: '̵' })">
S̵
</n-button>
<n-button title="Math double-struck" @click="formatTextAreaSelection('doubleStruck', { })">
𝔻
</n-button>
<n-button title="Fullwidth" @click="formatTextAreaSelection('fullwidth', { })">
</n-button>
<n-button title="Math Fraktur" @click="formatTextAreaSelection('fraktur', { })">
𝔉
</n-button>
<n-button title="Math Fraktur bold" @click="formatTextAreaSelection('boldFraktur', { })">
𝕱
</n-button>
<n-button title="Script" @click="formatTextAreaSelection('script', { })">
𝒮
</n-button>
<n-button title="Bold script" @click="formatTextAreaSelection('boldScript', { })">
𝓢
</n-button>
<n-button title="Circled" @click="formatTextAreaSelection('circled', { })">
</n-button>
<n-button title="Circled negative" @click="formatTextAreaSelection('circledNegative', { })">
🅒
</n-button>
<n-button title="Squared" @click="formatTextAreaSelection('squared', { })">
🅂
</n-button>
<n-button title="Squared negative" @click="formatTextAreaSelection('squaredNegative', { })">
🆂
</n-button>
<n-button title="Parenthesized" @click="formatTextAreaSelection('parenthesized', { })">
</n-button>
<n-button title="Small caps" @click="formatTextAreaSelection('smallCaps', { })">
</n-button>
<n-button title="Subscript" @click="formatTextAreaSelection('subscript', { })">
ₛᵤᵦ
</n-button>
<n-button title="Superscript" @click="formatTextAreaSelection('superscript', { })">
ˢᵘᵖ
</n-button>
<n-button title="Inverted" @click="formatTextAreaSelection('inverted', { })">
</n-button>
<n-button title="Rotated Left" @click="formatTextAreaSelection('rotatedLeft', { })">
</n-button>
<n-button title="Rotated Right" @click="formatTextAreaSelection('rotatedRight', { })">
</n-button>
<n-button title="Mirrored" @click="formatTextAreaSelection('mirrored', { })">
Я
</n-button>
<n-button title="Reverse" @click="formatTextAreaSelection(undefined, { reverse: true })">
<!-- Left/Right Arrow Icon -->
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z" />
</svg>
</n-button>
<n-button title="Inverted Reverse" @click="formatTextAreaSelection('inverted', { reverse: true })">
<!-- Left/Right Arrow Icon -->
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M377.941 169.941V216H134.059v-46.059c0-21.382-25.851-32.09-40.971-16.971L7.029 239.029c-9.373 9.373-9.373 24.568 0 33.941l86.059 86.059c15.119 15.119 40.971 4.411 40.971-16.971V296h243.882v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.568 0-33.941l-86.059-86.059c-15.119-15.12-40.971-4.412-40.971 16.97z" />
</svg>
</n-button>
<n-button title="Rotated Right Reverse" @click="formatTextAreaSelection('rotatedRight', { reverse: true })">
<!-- Left/Right Arrow Icon -->
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M377.941 169.941V216H134.059v-46.059c0-21.382-25.851-32.09-40.971-16.971L7.029 239.029c-9.373 9.373-9.373 24.568 0 33.941l86.059 86.059c15.119 15.119 40.971 4.411 40.971-16.971V296h243.882v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.568 0-33.941l-86.059-86.059c-15.119-15.12-40.971-4.412-40.971 16.97z" />
</svg>
</n-button>
<n-button title="Mirrored Reverse" @click="formatTextAreaSelection('mirrored', { reverse: true })">
Я
<!-- Left/Right Arrow Icon -->
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M377.941 169.941V216H134.059v-46.059c0-21.382-25.851-32.09-40.971-16.971L7.029 239.029c-9.373 9.373-9.373 24.568 0 33.941l86.059 86.059c15.119 15.119 40.971 4.411 40.971-16.971V296h243.882v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.568 0-33.941l-86.059-86.059c-15.119-15.12-40.971-4.412-40.971 16.97z" />
</svg>
</n-button>
</n-space>
</details>
<textarea
ref="textArea" :value="formattedText" rows="12" mb-3 mt-3 w-full
spellcheck="false"
style="font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;"
/>
<div text-center>
<c-button @click="copyFormattedText()">
<n-icon size="22">
<Copy />
</n-icon>
</c-button>
</div>
</div>
</template>