@oamm/textor 1.0.9 → 1.0.11
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/dist/bin/textor.js +165 -43
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +150 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +150 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/textor.js
CHANGED
|
@@ -647,7 +647,8 @@ async function verifyFileIntegrity(filePath, expectedHash, options = {}) {
|
|
|
647
647
|
acceptChanges = false,
|
|
648
648
|
normalization = 'normalizeEOL',
|
|
649
649
|
owner = null,
|
|
650
|
-
actualOwner = null
|
|
650
|
+
actualOwner = null,
|
|
651
|
+
signatures = []
|
|
651
652
|
} = options;
|
|
652
653
|
|
|
653
654
|
if (force) return { valid: true };
|
|
@@ -660,7 +661,7 @@ async function verifyFileIntegrity(filePath, expectedHash, options = {}) {
|
|
|
660
661
|
};
|
|
661
662
|
}
|
|
662
663
|
|
|
663
|
-
const isGenerated = await isTextorGenerated(filePath);
|
|
664
|
+
const isGenerated = await isTextorGenerated(filePath, signatures);
|
|
664
665
|
if (!isGenerated) {
|
|
665
666
|
return {
|
|
666
667
|
valid: false,
|
|
@@ -693,7 +694,7 @@ async function verifyFileIntegrity(filePath, expectedHash, options = {}) {
|
|
|
693
694
|
}
|
|
694
695
|
|
|
695
696
|
async function safeDelete(filePath, options = {}) {
|
|
696
|
-
const { force = false, expectedHash = null, acceptChanges = false, owner = null, actualOwner = null } = options;
|
|
697
|
+
const { force = false, expectedHash = null, acceptChanges = false, owner = null, actualOwner = null, signatures = [] } = options;
|
|
697
698
|
|
|
698
699
|
if (!existsSync(filePath)) {
|
|
699
700
|
return { deleted: false, reason: 'not-found' };
|
|
@@ -703,7 +704,8 @@ async function safeDelete(filePath, options = {}) {
|
|
|
703
704
|
force,
|
|
704
705
|
acceptChanges,
|
|
705
706
|
owner,
|
|
706
|
-
actualOwner
|
|
707
|
+
actualOwner,
|
|
708
|
+
signatures
|
|
707
709
|
});
|
|
708
710
|
if (!integrity.valid) {
|
|
709
711
|
return { deleted: false, reason: integrity.reason, message: integrity.message };
|
|
@@ -745,7 +747,8 @@ async function isSafeToDeleteDir(dirPath, stateFiles = {}, options = {}) {
|
|
|
745
747
|
const fileState = stateFiles[normalizedPath];
|
|
746
748
|
const integrity = await verifyFileIntegrity(filePath, fileState?.hash, {
|
|
747
749
|
...options,
|
|
748
|
-
actualOwner: fileState?.owner
|
|
750
|
+
actualOwner: fileState?.owner,
|
|
751
|
+
signatures: options.signatures || []
|
|
749
752
|
});
|
|
750
753
|
return integrity.valid;
|
|
751
754
|
})
|
|
@@ -841,7 +844,8 @@ async function safeMove(fromPath, toPath, options = {}) {
|
|
|
841
844
|
acceptChanges,
|
|
842
845
|
normalization,
|
|
843
846
|
owner,
|
|
844
|
-
actualOwner
|
|
847
|
+
actualOwner,
|
|
848
|
+
signatures: options.signatures || []
|
|
845
849
|
});
|
|
846
850
|
if (!integrity.valid) {
|
|
847
851
|
throw new Error(integrity.message);
|
|
@@ -1136,6 +1140,68 @@ ${layoutOpening}
|
|
|
1136
1140
|
`;
|
|
1137
1141
|
}
|
|
1138
1142
|
|
|
1143
|
+
function mergeRouteTemplate(existingContent, featureImportPath, featureComponentName, layoutName) {
|
|
1144
|
+
let content = existingContent;
|
|
1145
|
+
|
|
1146
|
+
// 1. Add import
|
|
1147
|
+
const importLine = `import ${featureComponentName} from '${featureImportPath}';`;
|
|
1148
|
+
if (!content.includes(importLine)) {
|
|
1149
|
+
// Find the second --- which marks the end of frontmatter
|
|
1150
|
+
const lines = content.split('\n');
|
|
1151
|
+
let frontMatterEndLine = -1;
|
|
1152
|
+
let dashCount = 0;
|
|
1153
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1154
|
+
if (lines[i].trim() === '---') {
|
|
1155
|
+
dashCount++;
|
|
1156
|
+
if (dashCount === 2) {
|
|
1157
|
+
frontMatterEndLine = i;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (frontMatterEndLine !== -1) {
|
|
1164
|
+
lines.splice(frontMatterEndLine, 0, importLine);
|
|
1165
|
+
content = lines.join('\n');
|
|
1166
|
+
} else if (content.includes('---')) {
|
|
1167
|
+
// If only one --- found, maybe it's just the start?
|
|
1168
|
+
// But standard Astro has two.
|
|
1169
|
+
// Fallback: insert at the beginning if no frontmatter end found
|
|
1170
|
+
content = importLine + '\n' + content;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// 2. Add component usage
|
|
1175
|
+
const componentTag = `<${featureComponentName} />`;
|
|
1176
|
+
if (!content.includes(componentTag)) {
|
|
1177
|
+
if (layoutName && layoutName !== 'none') {
|
|
1178
|
+
const layoutEndTag = `</${layoutName}>`;
|
|
1179
|
+
if (content.includes(layoutEndTag)) {
|
|
1180
|
+
const lines = content.split('\n');
|
|
1181
|
+
let layoutEndLine = -1;
|
|
1182
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1183
|
+
if (lines[i].includes(layoutEndTag)) {
|
|
1184
|
+
layoutEndLine = i;
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (layoutEndLine !== -1) {
|
|
1189
|
+
lines.splice(layoutEndLine, 0, ` ${componentTag}`);
|
|
1190
|
+
content = lines.join('\n');
|
|
1191
|
+
}
|
|
1192
|
+
} else {
|
|
1193
|
+
// Layout might be self-closing or missing end tag?
|
|
1194
|
+
// If it's Textor generated it should have it.
|
|
1195
|
+
content += `\n${componentTag}\n`;
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
content += `\n${componentTag}\n`;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return content;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1139
1205
|
/**
|
|
1140
1206
|
* Feature Template Variables:
|
|
1141
1207
|
* - componentName: Name of the feature component
|
|
@@ -1561,9 +1627,9 @@ async function addSectionToState(section) {
|
|
|
1561
1627
|
if (normalizedSection.featurePath) {
|
|
1562
1628
|
normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
|
|
1563
1629
|
}
|
|
1564
|
-
// Avoid duplicates by route
|
|
1630
|
+
// Avoid duplicates by route AND by featurePath
|
|
1565
1631
|
if (normalizedSection.route) {
|
|
1566
|
-
state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
|
|
1632
|
+
state.sections = state.sections.filter(s => s.route !== normalizedSection.route || s.featurePath !== normalizedSection.featurePath);
|
|
1567
1633
|
} else {
|
|
1568
1634
|
state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
|
|
1569
1635
|
}
|
|
@@ -1641,6 +1707,10 @@ function reconstructComponents(files, config) {
|
|
|
1641
1707
|
path: componentPath
|
|
1642
1708
|
});
|
|
1643
1709
|
}
|
|
1710
|
+
// Attribute ownership
|
|
1711
|
+
if (!files[filePath].owner) {
|
|
1712
|
+
files[filePath].owner = componentName;
|
|
1713
|
+
}
|
|
1644
1714
|
}
|
|
1645
1715
|
}
|
|
1646
1716
|
}
|
|
@@ -1684,28 +1754,34 @@ function reconstructSections(state, config) {
|
|
|
1684
1754
|
|
|
1685
1755
|
if (!sections.has(finalRoute)) {
|
|
1686
1756
|
// Try to find a matching feature by name
|
|
1687
|
-
const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute);
|
|
1757
|
+
const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute).toLowerCase();
|
|
1688
1758
|
// Look for a directory in features with same name or similar
|
|
1689
1759
|
const possibleFeaturePath = Object.keys(files).find(f => {
|
|
1690
1760
|
const nf = f.replace(/\\/g, '/');
|
|
1691
|
-
|
|
1761
|
+
if (!nf.startsWith(featuresRoot + '/')) return false;
|
|
1762
|
+
const relToFeatures = nf.slice(featuresRoot.length + 1);
|
|
1763
|
+
const segments = relToFeatures.toLowerCase().split('/');
|
|
1764
|
+
return segments.includes(routeName);
|
|
1692
1765
|
});
|
|
1693
1766
|
|
|
1694
1767
|
if (possibleFeaturePath) {
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1768
|
+
const relToFeatures = path.dirname(path.relative(featuresRoot, possibleFeaturePath)).replace(/\\/g, '/');
|
|
1769
|
+
const featurePath = relToFeatures === '.' ? featuresRoot : `${featuresRoot}/${relToFeatures}`;
|
|
1770
|
+
const featureName = path.basename(featurePath);
|
|
1771
|
+
|
|
1772
|
+
sections.set(finalRoute, {
|
|
1773
|
+
name: featureName,
|
|
1774
|
+
route: finalRoute,
|
|
1775
|
+
featurePath: featurePath,
|
|
1776
|
+
extension: path.extname(filePath)
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
// Attribute ownership to discovered files
|
|
1780
|
+
if (!files[filePath].owner) files[filePath].owner = finalRoute;
|
|
1781
|
+
for (const f in files) {
|
|
1782
|
+
if (f.startsWith(featurePath + '/') || f === featurePath) {
|
|
1783
|
+
if (!files[f].owner) files[f].owner = finalRoute;
|
|
1784
|
+
}
|
|
1709
1785
|
}
|
|
1710
1786
|
}
|
|
1711
1787
|
}
|
|
@@ -1986,7 +2062,16 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1986
2062
|
await saveState(state);
|
|
1987
2063
|
}
|
|
1988
2064
|
|
|
1989
|
-
if (routeFilePath)
|
|
2065
|
+
if (routeFilePath) {
|
|
2066
|
+
if (existsSync(routeFilePath)) {
|
|
2067
|
+
const configSignatures = Object.values(config.signatures || {});
|
|
2068
|
+
const isGenerated = await isTextorGenerated(routeFilePath, configSignatures);
|
|
2069
|
+
if (!isGenerated && !options.force) {
|
|
2070
|
+
throw new Error(`File already exists: ${routeFilePath}\nUse --force to overwrite.`);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1990
2075
|
await ensureNotExists(featureFilePath, options.force);
|
|
1991
2076
|
|
|
1992
2077
|
if (shouldCreateIndex) await ensureNotExists(indexFilePath, options.force);
|
|
@@ -2068,15 +2153,42 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2068
2153
|
routeContent = generateEndpointTemplate(featureComponentName);
|
|
2069
2154
|
routeSignature = getSignature(config, 'typescript');
|
|
2070
2155
|
} else {
|
|
2071
|
-
routeContent = generateRouteTemplate(
|
|
2072
|
-
layout,
|
|
2073
|
-
layoutImportPath,
|
|
2074
|
-
featureImportPath,
|
|
2075
|
-
featureComponentName,
|
|
2076
|
-
routeExtension,
|
|
2077
|
-
layoutProps
|
|
2078
|
-
);
|
|
2079
2156
|
routeSignature = getSignature(config, 'astro');
|
|
2157
|
+
|
|
2158
|
+
if (existsSync(routeFilePath)) {
|
|
2159
|
+
const existingContent = await readFile(routeFilePath, 'utf-8');
|
|
2160
|
+
// Strip existing signature if present
|
|
2161
|
+
let contentToMerge = existingContent;
|
|
2162
|
+
if (existingContent.startsWith(routeSignature)) {
|
|
2163
|
+
contentToMerge = existingContent.slice(routeSignature.length).trimStart();
|
|
2164
|
+
} else {
|
|
2165
|
+
// Check for generic signature if specific one doesn't match
|
|
2166
|
+
const genericSignature = '@generated by Textor';
|
|
2167
|
+
if (existingContent.includes(genericSignature)) {
|
|
2168
|
+
const lines = existingContent.split('\n');
|
|
2169
|
+
if (lines[0].includes(genericSignature)) {
|
|
2170
|
+
lines.shift();
|
|
2171
|
+
contentToMerge = lines.join('\n').trimStart();
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
routeContent = mergeRouteTemplate(
|
|
2177
|
+
contentToMerge,
|
|
2178
|
+
featureImportPath,
|
|
2179
|
+
featureComponentName,
|
|
2180
|
+
layout
|
|
2181
|
+
);
|
|
2182
|
+
} else {
|
|
2183
|
+
routeContent = generateRouteTemplate(
|
|
2184
|
+
layout,
|
|
2185
|
+
layoutImportPath,
|
|
2186
|
+
featureImportPath,
|
|
2187
|
+
featureComponentName,
|
|
2188
|
+
routeExtension,
|
|
2189
|
+
layoutProps
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2080
2192
|
}
|
|
2081
2193
|
}
|
|
2082
2194
|
|
|
@@ -2419,6 +2531,7 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2419
2531
|
|
|
2420
2532
|
const pagesRoot = resolvePath(config, 'pages');
|
|
2421
2533
|
const featuresRoot = resolvePath(config, 'features');
|
|
2534
|
+
const configSignatures = Object.values(config.signatures || {});
|
|
2422
2535
|
|
|
2423
2536
|
// Find route file in state if possible
|
|
2424
2537
|
let routeFilePath = null;
|
|
@@ -2468,7 +2581,8 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2468
2581
|
acceptChanges: options.acceptChanges,
|
|
2469
2582
|
normalization: config.hashing?.normalization,
|
|
2470
2583
|
owner: normalizedRoute,
|
|
2471
|
-
actualOwner: fileState?.owner
|
|
2584
|
+
actualOwner: fileState?.owner,
|
|
2585
|
+
signatures: configSignatures
|
|
2472
2586
|
});
|
|
2473
2587
|
|
|
2474
2588
|
if (result.deleted) {
|
|
@@ -2485,7 +2599,8 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2485
2599
|
stateFiles: state.files,
|
|
2486
2600
|
acceptChanges: options.acceptChanges,
|
|
2487
2601
|
normalization: config.hashing?.normalization,
|
|
2488
|
-
owner: normalizedRoute
|
|
2602
|
+
owner: normalizedRoute,
|
|
2603
|
+
signatures: configSignatures
|
|
2489
2604
|
});
|
|
2490
2605
|
|
|
2491
2606
|
if (result.deleted) {
|
|
@@ -2712,6 +2827,7 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2712
2827
|
|
|
2713
2828
|
const pagesRoot = resolvePath(config, 'pages');
|
|
2714
2829
|
const featuresRoot = resolvePath(config, 'features');
|
|
2830
|
+
const configSignatures = Object.values(config.signatures || {});
|
|
2715
2831
|
|
|
2716
2832
|
const fromSection = findSection(state, actualFromRoute);
|
|
2717
2833
|
const routeExtension = (fromSection && fromSection.extension) || config.naming.routeExtension;
|
|
@@ -2753,7 +2869,8 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2753
2869
|
expectedHash: routeFileState?.hash,
|
|
2754
2870
|
acceptChanges: options.acceptChanges,
|
|
2755
2871
|
owner: normalizedFromRoute,
|
|
2756
|
-
actualOwner: routeFileState?.owner
|
|
2872
|
+
actualOwner: routeFileState?.owner,
|
|
2873
|
+
signatures: configSignatures
|
|
2757
2874
|
});
|
|
2758
2875
|
movedFiles.push({ from: fromRoutePath, to: toRoutePath });
|
|
2759
2876
|
|
|
@@ -2850,7 +2967,8 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2850
2967
|
...options,
|
|
2851
2968
|
fromName: fromFeatureComponentName,
|
|
2852
2969
|
toName: toFeatureComponentName,
|
|
2853
|
-
owner: normalizedFromRoute
|
|
2970
|
+
owner: normalizedFromRoute,
|
|
2971
|
+
signatures: configSignatures
|
|
2854
2972
|
});
|
|
2855
2973
|
movedFiles.push({ from: fromFeaturePath, to: toFeaturePath });
|
|
2856
2974
|
|
|
@@ -3559,6 +3677,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3559
3677
|
async function removeComponentCommand(identifier, options) {
|
|
3560
3678
|
try {
|
|
3561
3679
|
const config = await loadConfig();
|
|
3680
|
+
const configSignatures = Object.values(config.signatures || {});
|
|
3562
3681
|
|
|
3563
3682
|
if (config.git?.requireCleanRepo && !await isRepoClean()) {
|
|
3564
3683
|
throw new Error('Git repository is not clean. Please commit or stash your changes before proceeding.');
|
|
@@ -3588,7 +3707,8 @@ async function removeComponentCommand(identifier, options) {
|
|
|
3588
3707
|
stateFiles: state.files,
|
|
3589
3708
|
acceptChanges: options.acceptChanges,
|
|
3590
3709
|
normalization: config.hashing?.normalization,
|
|
3591
|
-
owner: identifier
|
|
3710
|
+
owner: identifier,
|
|
3711
|
+
signatures: configSignatures
|
|
3592
3712
|
});
|
|
3593
3713
|
|
|
3594
3714
|
if (result.deleted || (result.reason === 'not-found' && component)) {
|
|
@@ -3637,7 +3757,8 @@ async function listSectionsCommand() {
|
|
|
3637
3757
|
console.log(' No pages directory found.');
|
|
3638
3758
|
} else {
|
|
3639
3759
|
const extensions = [config.naming.routeExtension, '.ts', '.js'];
|
|
3640
|
-
const
|
|
3760
|
+
const signatures = Object.values(config.signatures || {});
|
|
3761
|
+
const sections = await findGeneratedFiles(pagesRoot, extensions, signatures);
|
|
3641
3762
|
|
|
3642
3763
|
if (sections.length === 0) {
|
|
3643
3764
|
console.log(' No Textor-managed sections found.');
|
|
@@ -3741,7 +3862,7 @@ async function listSectionsCommand() {
|
|
|
3741
3862
|
}
|
|
3742
3863
|
}
|
|
3743
3864
|
|
|
3744
|
-
async function findGeneratedFiles(dir, extensions) {
|
|
3865
|
+
async function findGeneratedFiles(dir, extensions, signatures) {
|
|
3745
3866
|
const results = [];
|
|
3746
3867
|
const entries = await readdir(dir);
|
|
3747
3868
|
const exts = Array.isArray(extensions) ? extensions : [extensions];
|
|
@@ -3751,9 +3872,9 @@ async function findGeneratedFiles(dir, extensions) {
|
|
|
3751
3872
|
const stats = await stat(fullPath);
|
|
3752
3873
|
|
|
3753
3874
|
if (stats.isDirectory()) {
|
|
3754
|
-
results.push(...await findGeneratedFiles(fullPath, exts));
|
|
3875
|
+
results.push(...await findGeneratedFiles(fullPath, exts, signatures));
|
|
3755
3876
|
} else if (exts.some(ext => entry.endsWith(ext))) {
|
|
3756
|
-
if (await isTextorGenerated(fullPath)) {
|
|
3877
|
+
if (await isTextorGenerated(fullPath, signatures)) {
|
|
3757
3878
|
results.push(fullPath);
|
|
3758
3879
|
}
|
|
3759
3880
|
}
|
|
@@ -3811,11 +3932,12 @@ async function validateStateCommand(options) {
|
|
|
3811
3932
|
|
|
3812
3933
|
if (options.fix) {
|
|
3813
3934
|
let fixedCount = 0;
|
|
3935
|
+
const signatures = Object.values(config.signatures || {});
|
|
3814
3936
|
|
|
3815
3937
|
// Fix modified files if they still have the Textor signature
|
|
3816
3938
|
for (const mod of results.modified) {
|
|
3817
3939
|
const fullPath = path.join(process.cwd(), mod.path);
|
|
3818
|
-
if (await isTextorGenerated(fullPath)) {
|
|
3940
|
+
if (await isTextorGenerated(fullPath, signatures)) {
|
|
3819
3941
|
state.files[mod.path].hash = mod.newHash;
|
|
3820
3942
|
fixedCount++;
|
|
3821
3943
|
}
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|