@oamm/textor 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -279,6 +279,20 @@ pnpm textor normalize-state
279
279
  **Options:**
280
280
  - `--dry-run`: Print the normalized state without writing it.
281
281
 
282
+ ### prune-missing
283
+ Remove missing references from Textor state. This command identifies files that are tracked in the state but are no longer present on disk and removes them from `.textor/state.json`.
284
+
285
+ **Safe-by-default**: This command never deletes any files from your disk; it only updates the state tracker.
286
+
287
+ ```bash
288
+ pnpm textor prune-missing
289
+ ```
290
+
291
+ **Options:**
292
+ - `--dry-run`: Preview what would be removed from the state without making changes.
293
+ - `--yes`: Skip confirmation prompt.
294
+ - `--no-interactive`: Disable interactive prompts (useful for CI).
295
+
282
296
  ## šŸ—ļø Technical Architecture
283
297
 
284
298
  Textor is designed with enterprise-grade robustness, moving beyond simple scaffolding to provide a reliable refactoring engine.
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import { createHash } from 'crypto';
7
7
  import { exec } from 'child_process';
8
8
  import { promisify } from 'util';
9
+ import readline from 'readline';
9
10
 
10
11
  const CONFIG_DIR$1 = '.textor';
11
12
  const CONFIG_FILE = 'config.json';
@@ -1135,6 +1136,68 @@ ${layoutOpening}
1135
1136
  `;
1136
1137
  }
1137
1138
 
1139
+ function mergeRouteTemplate(existingContent, featureImportPath, featureComponentName, layoutName) {
1140
+ let content = existingContent;
1141
+
1142
+ // 1. Add import
1143
+ const importLine = `import ${featureComponentName} from '${featureImportPath}';`;
1144
+ if (!content.includes(importLine)) {
1145
+ // Find the second --- which marks the end of frontmatter
1146
+ const lines = content.split('\n');
1147
+ let frontMatterEndLine = -1;
1148
+ let dashCount = 0;
1149
+ for (let i = 0; i < lines.length; i++) {
1150
+ if (lines[i].trim() === '---') {
1151
+ dashCount++;
1152
+ if (dashCount === 2) {
1153
+ frontMatterEndLine = i;
1154
+ break;
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ if (frontMatterEndLine !== -1) {
1160
+ lines.splice(frontMatterEndLine, 0, importLine);
1161
+ content = lines.join('\n');
1162
+ } else if (content.includes('---')) {
1163
+ // If only one --- found, maybe it's just the start?
1164
+ // But standard Astro has two.
1165
+ // Fallback: insert at the beginning if no frontmatter end found
1166
+ content = importLine + '\n' + content;
1167
+ }
1168
+ }
1169
+
1170
+ // 2. Add component usage
1171
+ const componentTag = `<${featureComponentName} />`;
1172
+ if (!content.includes(componentTag)) {
1173
+ if (layoutName && layoutName !== 'none') {
1174
+ const layoutEndTag = `</${layoutName}>`;
1175
+ if (content.includes(layoutEndTag)) {
1176
+ const lines = content.split('\n');
1177
+ let layoutEndLine = -1;
1178
+ for (let i = lines.length - 1; i >= 0; i--) {
1179
+ if (lines[i].includes(layoutEndTag)) {
1180
+ layoutEndLine = i;
1181
+ break;
1182
+ }
1183
+ }
1184
+ if (layoutEndLine !== -1) {
1185
+ lines.splice(layoutEndLine, 0, ` ${componentTag}`);
1186
+ content = lines.join('\n');
1187
+ }
1188
+ } else {
1189
+ // Layout might be self-closing or missing end tag?
1190
+ // If it's Textor generated it should have it.
1191
+ content += `\n${componentTag}\n`;
1192
+ }
1193
+ } else {
1194
+ content += `\n${componentTag}\n`;
1195
+ }
1196
+ }
1197
+
1198
+ return content;
1199
+ }
1200
+
1138
1201
  /**
1139
1202
  * Feature Template Variables:
1140
1203
  * - componentName: Name of the feature component
@@ -1560,9 +1623,9 @@ async function addSectionToState(section) {
1560
1623
  if (normalizedSection.featurePath) {
1561
1624
  normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
1562
1625
  }
1563
- // Avoid duplicates by route OR by featurePath if route is null
1626
+ // Avoid duplicates by route AND by featurePath
1564
1627
  if (normalizedSection.route) {
1565
- state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
1628
+ state.sections = state.sections.filter(s => s.route !== normalizedSection.route || s.featurePath !== normalizedSection.featurePath);
1566
1629
  } else {
1567
1630
  state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
1568
1631
  }
@@ -1640,6 +1703,10 @@ function reconstructComponents(files, config) {
1640
1703
  path: componentPath
1641
1704
  });
1642
1705
  }
1706
+ // Attribute ownership
1707
+ if (!files[filePath].owner) {
1708
+ files[filePath].owner = componentName;
1709
+ }
1643
1710
  }
1644
1711
  }
1645
1712
  }
@@ -1683,28 +1750,34 @@ function reconstructSections(state, config) {
1683
1750
 
1684
1751
  if (!sections.has(finalRoute)) {
1685
1752
  // Try to find a matching feature by name
1686
- const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute);
1753
+ const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute).toLowerCase();
1687
1754
  // Look for a directory in features with same name or similar
1688
1755
  const possibleFeaturePath = Object.keys(files).find(f => {
1689
1756
  const nf = f.replace(/\\/g, '/');
1690
- return nf.startsWith(featuresRoot + '/') && nf.includes('/' + routeName + '/');
1757
+ if (!nf.startsWith(featuresRoot + '/')) return false;
1758
+ const relToFeatures = nf.slice(featuresRoot.length + 1);
1759
+ const segments = relToFeatures.toLowerCase().split('/');
1760
+ return segments.includes(routeName);
1691
1761
  });
1692
1762
 
1693
1763
  if (possibleFeaturePath) {
1694
- const featurePathParts = possibleFeaturePath.replace(/\\/g, '/').split('/');
1695
- const featuresBase = path.basename(featuresRoot);
1696
- const featureIndex = featurePathParts.indexOf(featuresBase) + 1;
1697
-
1698
- if (featureIndex > 0 && featureIndex < featurePathParts.length) {
1699
- const featureName = featurePathParts[featureIndex];
1700
- const featurePath = `${featuresRoot}/${featureName}`;
1701
-
1702
- sections.set(finalRoute, {
1703
- name: featureName,
1704
- route: finalRoute,
1705
- featurePath: featurePath,
1706
- extension: path.extname(filePath)
1707
- });
1764
+ const relToFeatures = path.dirname(path.relative(featuresRoot, possibleFeaturePath)).replace(/\\/g, '/');
1765
+ const featurePath = relToFeatures === '.' ? featuresRoot : `${featuresRoot}/${relToFeatures}`;
1766
+ const featureName = path.basename(featurePath);
1767
+
1768
+ sections.set(finalRoute, {
1769
+ name: featureName,
1770
+ route: finalRoute,
1771
+ featurePath: featurePath,
1772
+ extension: path.extname(filePath)
1773
+ });
1774
+
1775
+ // Attribute ownership to discovered files
1776
+ if (!files[filePath].owner) files[filePath].owner = finalRoute;
1777
+ for (const f in files) {
1778
+ if (f.startsWith(featurePath + '/') || f === featurePath) {
1779
+ if (!files[f].owner) files[f].owner = finalRoute;
1780
+ }
1708
1781
  }
1709
1782
  }
1710
1783
  }
@@ -1985,7 +2058,15 @@ async function addSectionCommand(route, featurePath, options) {
1985
2058
  await saveState(state);
1986
2059
  }
1987
2060
 
1988
- if (routeFilePath) await ensureNotExists(routeFilePath, options.force);
2061
+ if (routeFilePath) {
2062
+ if (existsSync(routeFilePath)) {
2063
+ const isGenerated = await isTextorGenerated(routeFilePath);
2064
+ if (!isGenerated && !options.force) {
2065
+ throw new Error(`File already exists: ${routeFilePath}\nUse --force to overwrite.`);
2066
+ }
2067
+ }
2068
+ }
2069
+
1989
2070
  await ensureNotExists(featureFilePath, options.force);
1990
2071
 
1991
2072
  if (shouldCreateIndex) await ensureNotExists(indexFilePath, options.force);
@@ -2067,15 +2148,42 @@ async function addSectionCommand(route, featurePath, options) {
2067
2148
  routeContent = generateEndpointTemplate(featureComponentName);
2068
2149
  routeSignature = getSignature(config, 'typescript');
2069
2150
  } else {
2070
- routeContent = generateRouteTemplate(
2071
- layout,
2072
- layoutImportPath,
2073
- featureImportPath,
2074
- featureComponentName,
2075
- routeExtension,
2076
- layoutProps
2077
- );
2078
2151
  routeSignature = getSignature(config, 'astro');
2152
+
2153
+ if (existsSync(routeFilePath)) {
2154
+ const existingContent = await readFile(routeFilePath, 'utf-8');
2155
+ // Strip existing signature if present
2156
+ let contentToMerge = existingContent;
2157
+ if (existingContent.startsWith(routeSignature)) {
2158
+ contentToMerge = existingContent.slice(routeSignature.length).trimStart();
2159
+ } else {
2160
+ // Check for generic signature if specific one doesn't match
2161
+ const genericSignature = '@generated by Textor';
2162
+ if (existingContent.includes(genericSignature)) {
2163
+ const lines = existingContent.split('\n');
2164
+ if (lines[0].includes(genericSignature)) {
2165
+ lines.shift();
2166
+ contentToMerge = lines.join('\n').trimStart();
2167
+ }
2168
+ }
2169
+ }
2170
+
2171
+ routeContent = mergeRouteTemplate(
2172
+ contentToMerge,
2173
+ featureImportPath,
2174
+ featureComponentName,
2175
+ layout
2176
+ );
2177
+ } else {
2178
+ routeContent = generateRouteTemplate(
2179
+ layout,
2180
+ layoutImportPath,
2181
+ featureImportPath,
2182
+ featureComponentName,
2183
+ routeExtension,
2184
+ layoutProps
2185
+ );
2186
+ }
2079
2187
  }
2080
2188
  }
2081
2189
 
@@ -3844,67 +3952,87 @@ async function validateStateCommand(options) {
3844
3952
  }
3845
3953
  }
3846
3954
 
3847
- async function statusCommand() {
3848
- try {
3849
- const config = await loadConfig();
3850
- const state = await loadState();
3851
-
3852
- const results = {
3853
- missing: [],
3854
- modified: [],
3855
- untracked: [], // Has signature, not in state
3856
- orphaned: [], // No signature, not in state
3857
- synced: 0
3858
- };
3955
+ /**
3956
+ * Computes the drift between the state and the actual files on disk.
3957
+ *
3958
+ * @param {import('./config.js').TextorConfig} config
3959
+ * @param {Object} state
3960
+ * @returns {Promise<{
3961
+ * missing: string[],
3962
+ * modified: string[],
3963
+ * untracked: string[],
3964
+ * orphaned: string[],
3965
+ * synced: number
3966
+ * }>}
3967
+ */
3968
+ async function getProjectStatus(config, state) {
3969
+ const results = {
3970
+ missing: [],
3971
+ modified: [],
3972
+ untracked: [], // Has signature, not in state
3973
+ orphaned: [], // No signature, not in state
3974
+ synced: 0
3975
+ };
3859
3976
 
3860
- const roots = [
3861
- resolvePath(config, 'pages'),
3862
- resolvePath(config, 'features'),
3863
- resolvePath(config, 'components')
3864
- ].map(p => path.resolve(p));
3977
+ const roots = [
3978
+ resolvePath(config, 'pages'),
3979
+ resolvePath(config, 'features'),
3980
+ resolvePath(config, 'components')
3981
+ ].map(p => path.resolve(p));
3865
3982
 
3866
- const diskFiles = new Set();
3867
- const configSignatures = Object.values(config.signatures || {});
3983
+ const diskFiles = new Set();
3984
+ const configSignatures = Object.values(config.signatures || {});
3868
3985
 
3869
- for (const root of roots) {
3870
- if (existsSync(root)) {
3871
- await scanDirectory(root, diskFiles);
3872
- }
3986
+ for (const root of roots) {
3987
+ if (existsSync(root)) {
3988
+ await scanDirectory(root, diskFiles);
3873
3989
  }
3990
+ }
3874
3991
 
3875
- // 1. Check state files against disk
3876
- for (const relativePath in state.files) {
3877
- const fullPath = path.join(process.cwd(), relativePath);
3878
-
3879
- if (!existsSync(fullPath)) {
3880
- results.missing.push(relativePath);
3881
- continue;
3882
- }
3992
+ // 1. Check state files against disk
3993
+ for (const relativePath in state.files) {
3994
+ const fullPath = path.join(process.cwd(), relativePath);
3995
+
3996
+ if (!existsSync(fullPath)) {
3997
+ results.missing.push(relativePath);
3998
+ continue;
3999
+ }
3883
4000
 
3884
- diskFiles.delete(relativePath);
4001
+ // It exists on disk, so it's not untracked/orphaned
4002
+ diskFiles.delete(relativePath);
3885
4003
 
3886
- const content = await readFile(fullPath, 'utf-8');
3887
- const currentHash = calculateHash(content, config.hashing?.normalization);
3888
- const fileData = state.files[relativePath];
4004
+ const content = await readFile(fullPath, 'utf-8');
4005
+ const currentHash = calculateHash(content, config.hashing?.normalization);
4006
+ const fileData = state.files[relativePath];
3889
4007
 
3890
- if (currentHash !== fileData.hash) {
3891
- results.modified.push(relativePath);
3892
- } else {
3893
- results.synced++;
3894
- }
4008
+ if (currentHash !== fileData.hash) {
4009
+ results.modified.push(relativePath);
4010
+ } else {
4011
+ results.synced++;
3895
4012
  }
4013
+ }
3896
4014
 
3897
- // 2. Check remaining disk files
3898
- for (const relativePath of diskFiles) {
3899
- const fullPath = path.join(process.cwd(), relativePath);
3900
- const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3901
-
3902
- if (isGenerated) {
3903
- results.untracked.push(relativePath);
3904
- } else {
3905
- results.orphaned.push(relativePath);
3906
- }
4015
+ // 2. Check remaining disk files
4016
+ for (const relativePath of diskFiles) {
4017
+ const fullPath = path.join(process.cwd(), relativePath);
4018
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
4019
+
4020
+ if (isGenerated) {
4021
+ results.untracked.push(relativePath);
4022
+ } else {
4023
+ results.orphaned.push(relativePath);
3907
4024
  }
4025
+ }
4026
+
4027
+ return results;
4028
+ }
4029
+
4030
+ async function statusCommand() {
4031
+ try {
4032
+ const config = await loadConfig();
4033
+ const state = await loadState();
4034
+
4035
+ const results = await getProjectStatus(config, state);
3908
4036
 
3909
4037
  // Reporting
3910
4038
  console.log('Textor Status Report:');
@@ -4329,6 +4457,71 @@ async function normalizeStateCommand(options) {
4329
4457
  }
4330
4458
  }
4331
4459
 
4460
+ /**
4461
+ * Removes missing references from Textor state.
4462
+ * @param {Object} options
4463
+ * @param {boolean} options.dryRun
4464
+ * @param {boolean} options.yes
4465
+ */
4466
+ async function pruneMissingCommand(options = {}) {
4467
+ try {
4468
+ const config = await loadConfig();
4469
+ const state = await loadState();
4470
+
4471
+ const results = await getProjectStatus(config, state);
4472
+
4473
+ if (results.missing.length === 0) {
4474
+ console.log('No missing references found.');
4475
+ return;
4476
+ }
4477
+
4478
+ console.log(`Found ${results.missing.length} missing references:`);
4479
+ results.missing.forEach(f => console.log(` - ${f}`));
4480
+
4481
+ if (options.dryRun) {
4482
+ console.log('\nDry run: no changes applied to state.');
4483
+ return;
4484
+ }
4485
+
4486
+ if (!options.yes && options.interactive !== false && process.stdin.isTTY && process.env.NODE_ENV !== 'test') {
4487
+ const rl = readline.createInterface({
4488
+ input: process.stdin,
4489
+ output: process.stdout
4490
+ });
4491
+
4492
+ const confirmed = await new Promise(resolve => {
4493
+ rl.question('\nDo you want to proceed with pruning? (y/N) ', (answer) => {
4494
+ rl.close();
4495
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
4496
+ });
4497
+ });
4498
+
4499
+ if (!confirmed) {
4500
+ console.log('Aborted.');
4501
+ return;
4502
+ }
4503
+ }
4504
+
4505
+ for (const relPath of results.missing) {
4506
+ delete state.files[relPath];
4507
+ }
4508
+
4509
+ // Reconstruct metadata
4510
+ state.components = reconstructComponents(state.files, config);
4511
+ state.sections = reconstructSections(state, config);
4512
+
4513
+ await saveState(state);
4514
+ console.log(`\nāœ“ Successfully removed ${results.missing.length} missing references from state.`);
4515
+
4516
+ } catch (error) {
4517
+ console.error('Error:', error.message);
4518
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
4519
+ process.exit(1);
4520
+ }
4521
+ throw error;
4522
+ }
4523
+ }
4524
+
4332
4525
  const program = new Command();
4333
4526
 
4334
4527
  program
@@ -4466,5 +4659,13 @@ program
4466
4659
  .option('--dry-run', 'Show the normalized state without writing to disk')
4467
4660
  .action(normalizeStateCommand);
4468
4661
 
4662
+ program
4663
+ .command('prune-missing')
4664
+ .description('Remove missing files from state (files that are in state but not on disk)')
4665
+ .option('--dry-run', 'Show what would be removed without applying')
4666
+ .option('--yes', 'Skip confirmation')
4667
+ .option('--no-interactive', 'Disable interactive prompts')
4668
+ .action(pruneMissingCommand);
4669
+
4469
4670
  program.parse();
4470
4671
  //# sourceMappingURL=textor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}