@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.
@@ -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 OR by featurePath if route is null
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
- return nf.startsWith(featuresRoot + '/') && nf.includes('/' + routeName + '/');
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 featurePathParts = possibleFeaturePath.replace(/\\/g, '/').split('/');
1696
- const featuresBase = path.basename(featuresRoot);
1697
- const featureIndex = featurePathParts.indexOf(featuresBase) + 1;
1698
-
1699
- if (featureIndex > 0 && featureIndex < featurePathParts.length) {
1700
- const featureName = featurePathParts[featureIndex];
1701
- const featurePath = `${featuresRoot}/${featureName}`;
1702
-
1703
- sections.set(finalRoute, {
1704
- name: featureName,
1705
- route: finalRoute,
1706
- featurePath: featurePath,
1707
- extension: path.extname(filePath)
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) await ensureNotExists(routeFilePath, options.force);
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 sections = await findGeneratedFiles(pagesRoot, extensions);
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}