diff --git a/components.d.ts b/components.d.ts
index f2c3146f..7424161c 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -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']
diff --git a/src/tools/index.ts b/src/tools/index.ts
index aa861c93..249c05d0 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -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,
],
},
{
diff --git a/src/tools/unicode-formatter/index.ts b/src/tools/unicode-formatter/index.ts
new file mode 100644
index 00000000..69f081ec
--- /dev/null
+++ b/src/tools/unicode-formatter/index.ts
@@ -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'),
+});
diff --git a/src/tools/unicode-formatter/unicode-fonts.json b/src/tools/unicode-formatter/unicode-fonts.json
new file mode 100644
index 00000000..7efc8da4
--- /dev/null
+++ b/src/tools/unicode-formatter/unicode-fonts.json
@@ -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โดฐแแแแโฃโ<แฌรโค๐โดโตโบ|`โแแดเฏจแ โเฎฎแ๐ฉแโคโแดแดโดฐแแแแ๐โ<แฌรโค๐โ_โเฒฝ"
+ }
\ No newline at end of file
diff --git a/src/tools/unicode-formatter/unicode-formatter.service.test.ts b/src/tools/unicode-formatter/unicode-formatter.service.test.ts
new file mode 100644
index 00000000..f4d0380e
--- /dev/null
+++ b/src/tools/unicode-formatter/unicode-formatter.service.test.ts
@@ -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,
+ });
+ });
+});
diff --git a/src/tools/unicode-formatter/unicode-formatter.service.ts b/src/tools/unicode-formatter/unicode-formatter.service.ts
new file mode 100644
index 00000000..0490dcaa
--- /dev/null
+++ b/src/tools/unicode-formatter/unicode-formatter.service.ts
@@ -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();
+}
diff --git a/src/tools/unicode-formatter/unicode-formatter.vue b/src/tools/unicode-formatter/unicode-formatter.vue
new file mode 100644
index 00000000..26ef20f0
--- /dev/null
+++ b/src/tools/unicode-formatter/unicode-formatter.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ๐
+
+
+ ๐
+
+
+ ๐ผ
+
+
+ U
+
+
+ S
+
+
+
+
+
+ More fonts
+
+
+ ๐ฌ
+
+
+ ๐ฝ๐
+
+
+ ๐
+
+
+ ๐ผ
+
+
+ ๐ฉ๐ฐ
+
+
+ Uฬณ
+
+
+ Sฬต
+
+
+ ๐ป
+
+
+ ๏ผฆ
+
+
+ ๐
+
+
+ ๐ฑ
+
+
+ ๐ฎ
+
+
+ ๐ข
+
+
+ โธ
+
+
+ ๐
+
+
+ ๐
+
+
+ ๐
+
+
+ โซ
+
+
+ Aส
+
+
+ โแตคแตฆ
+
+
+ หขแตแต
+
+
+ โ
+
+
+ แ
+
+
+ แ
+
+
+ ะฏ
+
+
+
+
+
+
+ โ
+
+
+
+
+ แ
+
+
+
+
+ ะฏ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+