@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 +14 -0
- package/dist/bin/textor.js +278 -77
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +187 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +187 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
package/dist/bin/textor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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)
|
|
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
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
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
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3977
|
+
const roots = [
|
|
3978
|
+
resolvePath(config, 'pages'),
|
|
3979
|
+
resolvePath(config, 'features'),
|
|
3980
|
+
resolvePath(config, 'components')
|
|
3981
|
+
].map(p => path.resolve(p));
|
|
3865
3982
|
|
|
3866
|
-
|
|
3867
|
-
|
|
3983
|
+
const diskFiles = new Set();
|
|
3984
|
+
const configSignatures = Object.values(config.signatures || {});
|
|
3868
3985
|
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
}
|
|
3986
|
+
for (const root of roots) {
|
|
3987
|
+
if (existsSync(root)) {
|
|
3988
|
+
await scanDirectory(root, diskFiles);
|
|
3873
3989
|
}
|
|
3990
|
+
}
|
|
3874
3991
|
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
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
|
-
|
|
4001
|
+
// It exists on disk, so it's not untracked/orphaned
|
|
4002
|
+
diskFiles.delete(relativePath);
|
|
3885
4003
|
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
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
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
}
|
|
4008
|
+
if (currentHash !== fileData.hash) {
|
|
4009
|
+
results.modified.push(relativePath);
|
|
4010
|
+
} else {
|
|
4011
|
+
results.synced++;
|
|
3895
4012
|
}
|
|
4013
|
+
}
|
|
3896
4014
|
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
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
|
package/dist/bin/textor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|