@ijonis/geo-lint 0.1.3 → 0.1.4

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/cli.cjs CHANGED
@@ -448,7 +448,7 @@ function createLinkExtractor(siteUrl) {
448
448
  // src/utils/slug-resolver.ts
449
449
  var import_node_fs4 = require("fs");
450
450
  var import_node_path3 = require("path");
451
- function extractSlugFromFile(filePath) {
451
+ function extractSlugFromFile(filePath, defaultLocale = "de") {
452
452
  try {
453
453
  const content = (0, import_node_fs4.readFileSync)(filePath, "utf-8");
454
454
  if (!content.startsWith("---")) return null;
@@ -462,7 +462,7 @@ function extractSlugFromFile(filePath) {
462
462
  if (draftMatch) return null;
463
463
  return {
464
464
  slug: slugMatch[1].trim(),
465
- locale: localeMatch ? localeMatch[1].trim() : "de"
465
+ locale: localeMatch ? localeMatch[1].trim() : defaultLocale
466
466
  };
467
467
  } catch {
468
468
  return null;
@@ -491,12 +491,12 @@ function scanRawContentPermalinks(knownSlugs, contentPaths) {
491
491
  const contentDir = (0, import_node_path3.join)(projectRoot, pathConfig.dir);
492
492
  if (!(0, import_node_fs4.existsSync)(contentDir)) continue;
493
493
  const urlPrefix = pathConfig.urlPrefix ?? "/";
494
+ const defaultLocale = pathConfig.defaultLocale ?? "de";
494
495
  for (const file of findFilesInDir(contentDir, ".mdx")) {
495
- const meta = extractSlugFromFile(file);
496
+ const meta = extractSlugFromFile(file, defaultLocale);
496
497
  if (!meta) continue;
497
498
  let permalink;
498
- const defaultLocale = pathConfig.defaultLocale ?? "de";
499
- if (meta.locale !== defaultLocale && meta.locale !== "de") {
499
+ if (meta.locale !== defaultLocale) {
500
500
  permalink = `/${meta.locale}${urlPrefix}${meta.slug}`.replace(/\/+/g, "/");
501
501
  } else {
502
502
  permalink = `${urlPrefix}${meta.slug}`.replace(/\/+/g, "/");
@@ -1221,7 +1221,7 @@ function countSentences(text) {
1221
1221
  }
1222
1222
 
1223
1223
  // src/utils/readability.ts
1224
- function estimateSyllables(word) {
1224
+ function estimateSyllables(word, locale = "de") {
1225
1225
  const lower = word.toLowerCase();
1226
1226
  const vowelPattern = /[aeiouyäöü]+/gi;
1227
1227
  const matches = lower.match(vowelPattern);
@@ -1229,17 +1229,21 @@ function estimateSyllables(word) {
1229
1229
  return 1;
1230
1230
  }
1231
1231
  let count = matches.length;
1232
- if (lower.endsWith("e") && count > 1) {
1233
- count -= 0.5;
1234
- }
1235
- if (word.length > 12) {
1236
- count = Math.max(count, Math.ceil(word.length / 4));
1232
+ const lang = locale.toLowerCase().slice(0, 2);
1233
+ if (lang === "en") {
1234
+ if (lower.endsWith("e") && count > 1) {
1235
+ count -= 0.5;
1236
+ }
1237
+ } else {
1238
+ if (word.length > 12) {
1239
+ count = Math.max(count, Math.ceil(word.length / 4));
1240
+ }
1237
1241
  }
1238
1242
  return Math.max(1, Math.round(count));
1239
1243
  }
1240
- function countSyllables(text) {
1244
+ function countSyllables(text, locale) {
1241
1245
  const words = text.split(/\s+/).filter((word) => word.length > 0).filter((word) => /\w/.test(word));
1242
- return words.reduce((total, word) => total + estimateSyllables(word), 0);
1246
+ return words.reduce((total, word) => total + estimateSyllables(word, locale), 0);
1243
1247
  }
1244
1248
  function averageWordLength(text) {
1245
1249
  const words = text.split(/\s+/).filter((word) => word.length > 0).filter((word) => /\w/.test(word));
@@ -1247,11 +1251,35 @@ function averageWordLength(text) {
1247
1251
  const totalLength = words.reduce((sum, word) => sum + word.length, 0);
1248
1252
  return totalLength / words.length;
1249
1253
  }
1250
- function calculateReadability(mdxBody) {
1254
+ var FLESCH_COEFFICIENTS = {
1255
+ en: { intercept: 206.835, aslWeight: 1.015, aswWeight: 84.6 },
1256
+ de: { intercept: 180, aslWeight: 1, aswWeight: 58.5 }
1257
+ };
1258
+ var INTERPRETATION_BANDS = {
1259
+ en: [
1260
+ { min: 90, label: "Very easy to read" },
1261
+ { min: 80, label: "Easy to read" },
1262
+ { min: 70, label: "Fairly easy to read" },
1263
+ { min: 60, label: "Standard" },
1264
+ { min: 50, label: "Fairly difficult" },
1265
+ { min: 30, label: "Difficult" },
1266
+ { min: 0, label: "Very difficult" }
1267
+ ],
1268
+ de: [
1269
+ { min: 70, label: "Very easy to read" },
1270
+ { min: 60, label: "Easy to read" },
1271
+ { min: 50, label: "Fairly easy to read" },
1272
+ { min: 40, label: "Standard" },
1273
+ { min: 30, label: "Fairly difficult" },
1274
+ { min: 20, label: "Difficult" },
1275
+ { min: 0, label: "Very difficult" }
1276
+ ]
1277
+ };
1278
+ function calculateReadability(mdxBody, locale = "de") {
1251
1279
  const plainText = stripMarkdown(mdxBody);
1252
1280
  const wordCount = countWords(mdxBody);
1253
1281
  const sentenceCount = countSentences(mdxBody);
1254
- const syllableCount = countSyllables(plainText);
1282
+ const syllableCount = countSyllables(plainText, locale);
1255
1283
  if (wordCount === 0 || sentenceCount === 0) {
1256
1284
  return {
1257
1285
  score: 0,
@@ -1264,23 +1292,26 @@ function calculateReadability(mdxBody) {
1264
1292
  const avgSentenceLength = wordCount / sentenceCount;
1265
1293
  const avgSyllablesPerWord = syllableCount / wordCount;
1266
1294
  const avgWordLen = averageWordLength(plainText);
1267
- const score = Math.round(180 - avgSentenceLength - 58.5 * avgSyllablesPerWord);
1295
+ const lang = locale.toLowerCase().slice(0, 2);
1296
+ const coefficients = FLESCH_COEFFICIENTS[lang] ?? FLESCH_COEFFICIENTS.de;
1297
+ const score = Math.round(
1298
+ coefficients.intercept - coefficients.aslWeight * avgSentenceLength - coefficients.aswWeight * avgSyllablesPerWord
1299
+ );
1268
1300
  const clampedScore = Math.max(0, Math.min(100, score));
1269
1301
  return {
1270
1302
  score: clampedScore,
1271
1303
  avgSentenceLength: Math.round(avgSentenceLength * 10) / 10,
1272
1304
  avgSyllablesPerWord: Math.round(avgSyllablesPerWord * 100) / 100,
1273
1305
  avgWordLength: Math.round(avgWordLen * 10) / 10,
1274
- interpretation: getInterpretation(clampedScore)
1306
+ interpretation: getInterpretation(clampedScore, locale)
1275
1307
  };
1276
1308
  }
1277
- function getInterpretation(score) {
1278
- if (score >= 70) return "Very easy to read";
1279
- if (score >= 60) return "Easy to read";
1280
- if (score >= 50) return "Fairly easy to read";
1281
- if (score >= 40) return "Standard";
1282
- if (score >= 30) return "Fairly difficult";
1283
- if (score >= 20) return "Difficult";
1309
+ function getInterpretation(score, locale = "de") {
1310
+ const lang = locale.toLowerCase().slice(0, 2);
1311
+ const bands = INTERPRETATION_BANDS[lang] ?? INTERPRETATION_BANDS.de;
1312
+ for (const band of bands) {
1313
+ if (score >= band.min) return band.label;
1314
+ }
1284
1315
  return "Very difficult";
1285
1316
  }
1286
1317
  function isReadable(score, threshold) {
@@ -1321,7 +1352,8 @@ var lowReadability = {
1321
1352
  return [];
1322
1353
  }
1323
1354
  const c = context.thresholds ? resolveThresholds(context.thresholds, item.contentType).content : CONTENT_DEFAULTS;
1324
- const readability = calculateReadability(item.body);
1355
+ const locale = item.locale ?? context.defaultLocale ?? "de";
1356
+ const readability = calculateReadability(item.body, locale);
1325
1357
  if (!isReadable(readability.score, c.minReadabilityScore)) {
1326
1358
  const isExtreme = readability.score < 20;
1327
1359
  const severity = isExtreme ? "error" : "warning";
@@ -13605,9 +13637,9 @@ var DEFAULT_CONFIG2 = {
13605
13637
  function getLocaleConfig(locale) {
13606
13638
  return LOCALE_CONFIGS[locale.toLowerCase()] ?? DEFAULT_CONFIG2;
13607
13639
  }
13608
- function isComplexWord(normalizedWord, originalWord, config, frequencyList) {
13640
+ function isComplexWord(normalizedWord, originalWord, config, frequencyList, locale = "en") {
13609
13641
  if (normalizedWord.length <= config.minLength) return false;
13610
- if (estimateSyllables(normalizedWord) < config.minSyllables) return false;
13642
+ if (estimateSyllables(normalizedWord, locale) < config.minSyllables) return false;
13611
13643
  if (frequencyList?.has(normalizedWord)) return false;
13612
13644
  if (config.skipCapitalized && /^[A-Z]/.test(originalWord)) return false;
13613
13645
  return true;
@@ -13622,7 +13654,7 @@ function analyzeWordComplexity(body, locale = "en") {
13622
13654
  let complexCount = 0;
13623
13655
  for (const original of rawWords) {
13624
13656
  const normalized = original.toLowerCase();
13625
- if (isComplexWord(normalized, original, config, frequencyList)) {
13657
+ if (isComplexWord(normalized, original, config, frequencyList, locale)) {
13626
13658
  complexCount++;
13627
13659
  complexCounts.set(normalized, (complexCounts.get(normalized) ?? 0) + 1);
13628
13660
  }
@@ -15372,10 +15404,10 @@ var jargonDensity = {
15372
15404
  severity: "warning",
15373
15405
  category: "content",
15374
15406
  fixStrategy: "Replace complex or uncommon words with simpler alternatives",
15375
- run: (item) => {
15407
+ run: (item, context) => {
15376
15408
  const wordCount = countWords(item.body);
15377
15409
  if (wordCount < QUALITY_MIN_WORDS) return [];
15378
- const locale = item.locale ?? "en";
15410
+ const locale = item.locale ?? context.defaultLocale ?? "en";
15379
15411
  const analysis = analyzeJargonDensity(item.body, locale);
15380
15412
  if (analysis.density >= JARGON_ERROR_THRESHOLD) {
15381
15413
  const topWords = analysis.topJargonWords.slice(0, 3).map((w) => `"${w.word}" (${w.count}x)`).join(", ");
@@ -15516,10 +15548,10 @@ var lowTransitionWords = {
15516
15548
  severity: "warning",
15517
15549
  category: "content",
15518
15550
  fixStrategy: "Add transition words (however, therefore, for example, in addition) to connect ideas between sentences",
15519
- run: (item) => {
15551
+ run: (item, context) => {
15520
15552
  const wordCount = countWords(item.body);
15521
15553
  if (wordCount < TRANSITION_MIN_WORDS) return [];
15522
- const locale = item.locale ?? "en";
15554
+ const locale = item.locale ?? context.defaultLocale ?? "en";
15523
15555
  const analysis = analyzeTransitionWords(item.body, locale);
15524
15556
  if (analysis.totalSentences < 5) return [];
15525
15557
  if (analysis.ratio < TRANSITION_ERROR_THRESHOLD) {
@@ -15550,10 +15582,10 @@ var consecutiveStarts = {
15550
15582
  severity: "warning",
15551
15583
  category: "content",
15552
15584
  fixStrategy: "Vary sentence openings \u2014 use transition words, prepositional phrases, or reversed structures",
15553
- run: (item) => {
15585
+ run: (item, context) => {
15554
15586
  const wordCount = countWords(item.body);
15555
15587
  if (wordCount < CONSECUTIVE_STARTS_MIN_WORDS) return [];
15556
- const locale = item.locale ?? "en";
15588
+ const locale = item.locale ?? context.defaultLocale ?? "en";
15557
15589
  const analysis = analyzeSentenceBeginnings(item.body, locale);
15558
15590
  if (analysis.groups.length === 0) return [];
15559
15591
  const worst = analysis.groups.reduce((a, b) => a.count > b.count ? a : b);
@@ -15801,7 +15833,8 @@ async function lint(options = {}) {
15801
15833
  validSlugs: buildSlugRegistry(contentItems, config.staticRoutes, config.contentPaths),
15802
15834
  validImages: buildImageRegistry(config.imageDirectories),
15803
15835
  thresholds: config.thresholds,
15804
- geoEnabledContentTypes: config.geo.enabledContentTypes ?? ["blog"]
15836
+ geoEnabledContentTypes: config.geo.enabledContentTypes ?? ["blog"],
15837
+ defaultLocale: config.i18n.defaultLocale
15805
15838
  };
15806
15839
  if (isPretty) {
15807
15840
  printProgress(`Found ${context.validSlugs.size} valid URLs, ${context.validImages.size} images`);