|
| 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