Skip to content

Commit a84e8af

Browse files
committed
feat(new tool): Tabs to spaces
Fix CorentinTh#1722
1 parent 253231f commit a84e8af

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/tools/tab-to-spaces/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { IndentIncrease } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Tab to Spaces',
6+
path: '/tab-to-spaces',
7+
description: 'Convert tab to multiple spaces',
8+
keywords: ['tab', 'space'],
9+
component: () => import('./tab-to-spaces.vue'),
10+
icon: IndentIncrease,
11+
createdAt: new Date('2026-02-02'),
12+
category: 'Text',
13+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script setup lang="ts">
2+
import { useQueryParamOrStorage } from '@/composable/queryParams';
3+
4+
const input = ref('');
5+
6+
const spacesPerTab = useQueryParamOrStorage({ name: 'spaces', storageName: 'tabs-spc:s', defaultValue: 4 });
7+
const convertLeadingOnly = useQueryParamOrStorage({ name: 'leading', storageName: 'tabs-spc:l', defaultValue: false });
8+
const normalizeInner = useQueryParamOrStorage({ name: 'norm', storageName: 'tabs-spc:n', defaultValue: false });
9+
10+
// add 1 space for half width chars and 2 for others
11+
function getLen(str: string): number {
12+
let length = 0;
13+
for (let i = 0; i < str.length; i++) {
14+
const chr = str.charCodeAt(i);
15+
if ((chr >= 0x00 && chr <= 0x80)
16+
|| (chr >= 0xA0 && chr <= 0xFF)
17+
|| (chr === 0xF8F0)
18+
|| (chr >= 0xFF61 && chr <= 0xFF9F)
19+
|| (chr >= 0xF8F1 && chr <= 0xF8F3)) {
20+
length += 1;
21+
}
22+
else {
23+
length += 2;
24+
}
25+
}
26+
return length;
27+
};
28+
29+
function normalizeInnerSpacing(text: string): string {
30+
return text
31+
.split('\n')
32+
.map((line) => {
33+
// Preserve leading indentation
34+
const leading = line.match(/^\s*/)?.[0] ?? '';
35+
const rest = line.slice(leading.length);
36+
37+
// Collapse internal whitespace
38+
const normalized = rest.replace(/\s+/g, ' ');
39+
return leading + normalized;
40+
})
41+
.join('\n');
42+
}
43+
44+
function transformTabs(text: string, tabSize: number) {
45+
let t = text;
46+
while (t.includes('\t')) {
47+
const position = t.indexOf('\t');
48+
const before = t.slice(0, position);
49+
const beforeLen = getLen(before);
50+
const spacenum = tabSize - (beforeLen % tabSize);
51+
const temp = before + ' '.repeat(spacenum) + t.slice(position + 1);
52+
t = temp;
53+
}
54+
return t;
55+
}
56+
57+
const output = computed(() => {
58+
const tabSize = spacesPerTab.value;
59+
60+
let out = input.value;
61+
62+
if (convertLeadingOnly.value) {
63+
out = out
64+
.split('\n')
65+
.map((line) => {
66+
const match = line.match(/^\s+/);
67+
if (!match) {
68+
return line;
69+
}
70+
const leadingSpaces = match[0].length;
71+
return transformTabs(line.slice(0, leadingSpaces), tabSize) + line.slice(leadingSpaces);
72+
})
73+
.join('\n');
74+
}
75+
else {
76+
out = out
77+
.split('\n')
78+
.map((line) => {
79+
return transformTabs(line, tabSize);
80+
})
81+
.join('\n');
82+
}
83+
84+
if (normalizeInner.value) {
85+
out = normalizeInnerSpacing(out);
86+
}
87+
88+
return out;
89+
});
90+
</script>
91+
92+
<template>
93+
<div>
94+
<n-form label-placement="left" label-width="auto">
95+
<n-space justify="center">
96+
<n-form-item label="Spaces per tab">
97+
<n-input-number v-model:value="spacesPerTab" :min="1" :max="12" />
98+
</n-form-item>
99+
100+
<n-form-item label="Convert only leading tabs">
101+
<n-switch v-model:value="convertLeadingOnly" />
102+
</n-form-item>
103+
104+
<n-form-item label="Normalize inner spacing">
105+
<n-switch v-model:value="normalizeInner" />
106+
</n-form-item>
107+
</n-space>
108+
</n-form>
109+
110+
<c-card title="Input (with tabs)" mb-2>
111+
<c-input-text
112+
v-model:value="input"
113+
multiline
114+
rows="6"
115+
monospace
116+
placeholder="Paste text with tabs here..."
117+
/>
118+
</c-card>
119+
<c-card title="Output (with spaces)">
120+
<textarea-copyable
121+
:value="output"
122+
/>
123+
</c-card>
124+
</div>
125+
</template>

0 commit comments

Comments
 (0)