@jjlmoya/utils-nautical 1.18.0 → 1.20.0

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.
Files changed (64) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +9 -7
  5. package/src/tests/diacritics_density.test.ts +1 -1
  6. package/src/tests/inverted_punctuation.test.ts +1 -1
  7. package/src/tests/locale_completeness.test.ts +2 -2
  8. package/src/tests/script_density.test.ts +1 -1
  9. package/src/tests/seo_parity.test.ts +60 -0
  10. package/src/tests/title_quality.test.ts +1 -1
  11. package/src/tests/tool_validation.test.ts +2 -2
  12. package/src/tests/translation_copy.test.ts +124 -0
  13. package/src/tool/anchorScope/anchorScope.css +507 -0
  14. package/src/tool/anchorScope/bibliography.astro +6 -0
  15. package/src/tool/anchorScope/bibliography.ts +14 -0
  16. package/src/tool/anchorScope/component.astro +219 -0
  17. package/src/tool/anchorScope/constants.ts +51 -0
  18. package/src/tool/anchorScope/controller.ts +251 -0
  19. package/src/tool/anchorScope/dom-views.ts +106 -0
  20. package/src/tool/anchorScope/entry.ts +31 -0
  21. package/src/tool/anchorScope/evaluator.ts +61 -0
  22. package/src/tool/anchorScope/i18n/de.ts +228 -0
  23. package/src/tool/anchorScope/i18n/en.ts +223 -0
  24. package/src/tool/anchorScope/i18n/es.ts +223 -0
  25. package/src/tool/anchorScope/i18n/fr.ts +221 -0
  26. package/src/tool/anchorScope/i18n/id.ts +221 -0
  27. package/src/tool/anchorScope/i18n/it.ts +221 -0
  28. package/src/tool/anchorScope/i18n/ja.ts +221 -0
  29. package/src/tool/anchorScope/i18n/ko.ts +221 -0
  30. package/src/tool/anchorScope/i18n/nl.ts +221 -0
  31. package/src/tool/anchorScope/i18n/pl.ts +221 -0
  32. package/src/tool/anchorScope/i18n/pt.ts +221 -0
  33. package/src/tool/anchorScope/i18n/ru.ts +221 -0
  34. package/src/tool/anchorScope/i18n/sv.ts +221 -0
  35. package/src/tool/anchorScope/i18n/tr.ts +221 -0
  36. package/src/tool/anchorScope/i18n/zh.ts +221 -0
  37. package/src/tool/anchorScope/index.ts +10 -0
  38. package/src/tool/anchorScope/logic.test.ts +100 -0
  39. package/src/tool/anchorScope/logic.ts +84 -0
  40. package/src/tool/anchorScope/presets.ts +12 -0
  41. package/src/tool/anchorScope/seo.astro +14 -0
  42. package/src/tool/anchorScope/storage.ts +25 -0
  43. package/src/tool/anchorScope/types.ts +33 -0
  44. package/src/tool/anchorScope/ui.ts +63 -0
  45. package/src/tool/endurance/index.ts +2 -1
  46. package/src/tool/speedConverter/i18n/ru.ts +57 -0
  47. package/src/tool/speedConverter/i18n/tr.ts +20 -0
  48. package/src/tool/tideCalculator/i18n/es.ts +1 -1
  49. package/src/tool/tideCalculator/i18n/fr.ts +8 -0
  50. package/src/tool/underKeel/i18n/de.ts +4 -0
  51. package/src/tool/underKeel/i18n/es.ts +1 -1
  52. package/src/tool/underKeel/i18n/id.ts +4 -0
  53. package/src/tool/underKeel/i18n/it.ts +16 -0
  54. package/src/tool/underKeel/i18n/ja.ts +8 -0
  55. package/src/tool/underKeel/i18n/ko.ts +8 -0
  56. package/src/tool/underKeel/i18n/nl.ts +16 -0
  57. package/src/tool/underKeel/i18n/pl.ts +16 -0
  58. package/src/tool/underKeel/i18n/pt.ts +16 -0
  59. package/src/tool/underKeel/i18n/ru.ts +16 -0
  60. package/src/tool/underKeel/i18n/sv.ts +16 -0
  61. package/src/tool/underKeel/i18n/tr.ts +8 -0
  62. package/src/tool/underKeel/i18n/zh.ts +8 -0
  63. package/src/tools.ts +3 -0
  64. package/src/types.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-nautical",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -28,7 +28,7 @@
28
28
  "check": "astro check",
29
29
  "type-check": "astro check",
30
30
  "test": "vitest run",
31
- "preversion": "npm run lint && npm run test",
31
+ "preversion": "npm run lint && npm run test && npm run build",
32
32
  "postversion": "git push && git push --tags",
33
33
  "patch": "npm version patch",
34
34
  "minor": "npm version minor",
@@ -5,12 +5,13 @@ import { nauticalConverter } from '../tool/nauticalConverter/entry';
5
5
  import { sailArea } from '../tool/sailArea/entry';
6
6
  import { speedConverter } from '../tool/speedConverter/entry';
7
7
  import { endurance } from '../tool/endurance/entry';
8
+ import { anchorScope } from '../tool/anchorScope/entry';
8
9
 
9
10
  export type { CategoryLocaleContent };
10
11
 
11
12
  export const nauticalCategory: NauticalCategoryEntry = {
12
13
  icon: 'mdi:sail-boat',
13
- tools: [tideCalculator, underKeel, nauticalConverter, sailArea, speedConverter, endurance],
14
+ tools: [tideCalculator, underKeel, nauticalConverter, sailArea, speedConverter, endurance, anchorScope],
14
15
  i18n: {
15
16
  en: () => import('./i18n/en').then((m) => m.content),
16
17
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -10,6 +10,9 @@ export { tideCalculator } from './tool/tideCalculator/entry';
10
10
  export type { TideCalculatorUI, TideCalculatorLocaleContent } from './tool/tideCalculator/entry';
11
11
  export { underKeel } from './tool/underKeel/entry';
12
12
  export type { UnderKeelUI, UnderKeelLocaleContent } from './tool/underKeel/entry';
13
+ export { anchorScope } from './tool/anchorScope/entry';
14
+ export type { AnchorScopeLocaleContent } from './tool/anchorScope/entry';
15
+ export type { AnchorScopeUI } from './tool/anchorScope/ui';
13
16
  export { nauticalCategory } from './category';
14
17
  import { endurance } from './tool/endurance/entry';
15
18
  import { nauticalConverter } from './tool/nauticalConverter/entry';
@@ -17,4 +20,5 @@ import { sailArea } from './tool/sailArea/entry';
17
20
  import { speedConverter } from './tool/speedConverter/entry';
18
21
  import { tideCalculator } from './tool/tideCalculator/entry';
19
22
  import { underKeel } from './tool/underKeel/entry';
20
- export const ALL_ENTRIES = [endurance, nauticalConverter, sailArea, speedConverter, tideCalculator, underKeel];
23
+ import { anchorScope } from './tool/anchorScope/entry';
24
+ export const ALL_ENTRIES = [endurance, nauticalConverter, sailArea, speedConverter, tideCalculator, underKeel, anchorScope];
package/src/index.ts CHANGED
@@ -1,10 +1,4 @@
1
- export * from './tool/tideCalculator';
2
- export * from './tool/underKeel';
3
- export * from './tool/nauticalConverter';
4
- export * from './tool/sailArea';
5
- export * from './tool/speedConverter';
6
- export * from './tool/endurance';
7
- export * from './category';
1
+ export { nauticalCategory } from './category';
8
2
  export const NauticalCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
9
3
 
10
4
  export type {
@@ -18,6 +12,14 @@ export type {
18
12
  LocaleMap,
19
13
  NauticalToolEntry,
20
14
  NauticalCategoryEntry,
15
+ ToolDefinition,
21
16
  } from './types';
22
17
 
23
18
  export { ALL_ENTRIES, ALL_TOOLS } from './tools';
19
+ export { tideCalculator, TIDE_CALCULATOR_TOOL } from './tool/tideCalculator';
20
+ export { underKeel, UNDER_KEEL_TOOL } from './tool/underKeel';
21
+ export { nauticalConverter, NAUTICAL_CONVERTER_TOOL } from './tool/nauticalConverter';
22
+ export { sailArea, SAIL_AREA_TOOL } from './tool/sailArea';
23
+ export { speedConverter, SPEED_CONVERTER_TOOL } from './tool/speedConverter';
24
+ export { endurance, ENDURANCE_TOOL } from './tool/endurance';
25
+ export { anchorScope, ANCHOR_SCOPE_TOOL } from './tool/anchorScope';
@@ -96,7 +96,7 @@ describe('Diacritics density validation', () => {
96
96
  if (!loader) return;
97
97
 
98
98
  const content = await loader();
99
- const text = normalizeText(translatableContent(content as Record<string, unknown>));
99
+ const text = normalizeText(translatableContent((content as unknown) as Record<string, unknown>));
100
100
  const rule = DIACRITIC_RULES[typedLocale];
101
101
  const letters = letterCount(text);
102
102
  const matches = diacriticCount(text, typedLocale);
@@ -60,7 +60,7 @@ describe('Inverted punctuation validation', () => {
60
60
 
61
61
  const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
62
62
  const content = await loader();
63
- const strings = translatableStrings(content as Record<string, unknown>);
63
+ const strings = translatableStrings((content as unknown) as Record<string, unknown>);
64
64
  const missingQuestions = strings.flatMap((text) =>
65
65
  findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
66
66
  );
@@ -5,8 +5,8 @@ import { ALL_TOOLS } from '../tools';
5
5
  describe('Locale Completeness Validation', () => {
6
6
 
7
7
 
8
- it('all 10 tools registered', () => {
9
- expect(ALL_TOOLS.length).toBe(6);
8
+ it('all 7 tools registered', () => {
9
+ expect(ALL_TOOLS.length).toBe(7);
10
10
  });
11
11
  });
12
12
 
@@ -73,7 +73,7 @@ describe('Native script density validation', () => {
73
73
 
74
74
  const content = await loader();
75
75
  const rule = SCRIPT_RULES[typedLocale];
76
- const text = normalizeText(translatableContent(content as Record<string, unknown>));
76
+ const text = normalizeText(translatableContent((content as unknown) as Record<string, unknown>));
77
77
  const letters = letterCount(text);
78
78
  const matches = scriptCount(text, typedLocale);
79
79
  const ratio = scriptRatio(text, typedLocale);
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+ import type { KnownLocale } from '../types';
4
+
5
+ interface ExpectedCounts {
6
+ seo: number;
7
+ faq: number;
8
+ howTo: number;
9
+ }
10
+
11
+ function countItems(arr: unknown[] | undefined): number {
12
+ return arr?.length ?? 0;
13
+ }
14
+
15
+ async function verifyLocaleParity(
16
+ entry: typeof ALL_ENTRIES[number],
17
+ loc: KnownLocale,
18
+ expected: ExpectedCounts,
19
+ ): Promise<void> {
20
+ const locContent = await entry.i18n[loc]?.();
21
+ expect(locContent, `Locale ${loc} missing content`).toBeDefined();
22
+
23
+ const locSeoCount = countItems(locContent?.seo);
24
+ const locFaqCount = countItems(locContent?.faq);
25
+ const locHowToCount = countItems(locContent?.howTo);
26
+
27
+ expect(
28
+ locSeoCount,
29
+ `Locale ${loc} SEO sections count (${locSeoCount}) must match EN (${expected.seo})`,
30
+ ).toBe(expected.seo);
31
+ expect(
32
+ locFaqCount,
33
+ `Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
34
+ ).toBe(expected.faq);
35
+ expect(
36
+ locHowToCount,
37
+ `Locale ${loc} HowTo steps count (${locHowToCount}) must match EN (${expected.howTo})`,
38
+ ).toBe(expected.howTo);
39
+ }
40
+
41
+ describe('SEO & i18n Structural Parity Suite', () => {
42
+ ALL_ENTRIES.forEach((entry) => {
43
+ describe(`Tool: ${entry.id}`, () => {
44
+ it('all 15 locales should have identical SEO section counts and types as English', async () => {
45
+ const enContent = await entry.i18n.en?.();
46
+ expect(enContent).toBeDefined();
47
+ const expected: ExpectedCounts = {
48
+ seo: countItems(enContent?.seo),
49
+ faq: countItems(enContent?.faq),
50
+ howTo: countItems(enContent?.howTo),
51
+ };
52
+
53
+ const locales = Object.keys(entry.i18n) as KnownLocale[];
54
+ for (const loc of locales) {
55
+ await verifyLocaleParity(entry, loc, expected);
56
+ }
57
+ });
58
+ });
59
+ });
60
+ });
@@ -40,7 +40,7 @@ describe('Project Titles - Separator Validation', () => {
40
40
  for (const pattern of titlePatterns) {
41
41
  let match;
42
42
  while ((match = pattern.exec(content)) !== null) {
43
- const title = match[1];
43
+ const title = match[1] ?? '';
44
44
  if (title.includes('|') || title.includes('-')) {
45
45
  findings.push(title);
46
46
  }
@@ -5,8 +5,8 @@ const TOOL_ID_REGEX = /^[a-z0-9]+-?[a-z0-9]*$/;
5
5
 
6
6
  describe('Tool Validation Suite', () => {
7
7
  describe('Library Registration', () => {
8
- it('should have 6 tools in ALL_TOOLS', () => {
9
- expect(ALL_TOOLS.length).toBe(6);
8
+ it('should have 7 tools in ALL_TOOLS', () => {
9
+ expect(ALL_TOOLS.length).toBe(7);
10
10
  });
11
11
  });
12
12
 
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+
4
+ const COPY_THRESHOLD = 0.9;
5
+
6
+ const STRUCTURAL_KEYS = new Set([
7
+ '@context',
8
+ '@type',
9
+ 'applicationCategory',
10
+ 'columns',
11
+ 'highlight',
12
+ 'icon',
13
+ 'level',
14
+ 'operatingSystem',
15
+ 'position',
16
+ 'positive',
17
+ 'price',
18
+ 'priceCurrency',
19
+ 'slug',
20
+ 'trend',
21
+ 'type',
22
+ 'url',
23
+ 'value',
24
+ 'variant',
25
+ ]);
26
+
27
+ function normalizeText(value: string): string {
28
+ return value
29
+ .replace(/<[^>]*>/g, ' ')
30
+ .replace(/&(?:amp|lt|gt|quot|apos|nbsp);/gi, ' ')
31
+ .replace(/[\u2018\u2019]/g, "'")
32
+ .replace(/[\u201c\u201d]/g, '"')
33
+ .replace(/\s+/g, ' ')
34
+ .trim()
35
+ .toLocaleLowerCase();
36
+ }
37
+
38
+ function collectText(value: unknown, path: string, parts: string[]): void {
39
+ if (typeof value === 'string') {
40
+ const normalized = normalizeText(value);
41
+ if (normalized.length >= 2) parts.push(normalized);
42
+ return;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item, index) => collectText(item, `${path}[${index}]`, parts));
47
+ return;
48
+ }
49
+
50
+ if (!value || typeof value !== 'object') return;
51
+
52
+ Object.entries(value).forEach(([key, child]) => {
53
+ if (STRUCTURAL_KEYS.has(key)) return;
54
+ collectText(child, `${path}.${key}`, parts);
55
+ });
56
+ }
57
+
58
+ function localeCorpus(content: unknown): string {
59
+ if (!content || typeof content !== 'object') return '';
60
+
61
+ const record = content as Record<string, unknown>;
62
+ const parts: string[] = [];
63
+ collectText(record.title, 'title', parts);
64
+ collectText(record.description, 'description', parts);
65
+ collectText(record.faqTitle, 'faqTitle', parts);
66
+ collectText(record.faq, 'faq', parts);
67
+ collectText(record.seo, 'seo', parts);
68
+ collectText(record.schemas, 'schemas', parts);
69
+ return parts.join(' ');
70
+ }
71
+
72
+ function tokenCounts(text: string): Map<string, number> {
73
+ const counts = new Map<string, number>();
74
+ for (const token of text.match(/[\p{L}\p{N}]+/gu) ?? []) {
75
+ counts.set(token, (counts.get(token) ?? 0) + 1);
76
+ }
77
+ return counts;
78
+ }
79
+
80
+ function copySimilarity(left: string, right: string): number {
81
+ const leftCounts = tokenCounts(left);
82
+ const rightCounts = tokenCounts(right);
83
+ const leftTotal = [...leftCounts.values()].reduce((sum, count) => sum + count, 0);
84
+ const rightTotal = [...rightCounts.values()].reduce((sum, count) => sum + count, 0);
85
+ if (leftTotal === 0 || rightTotal === 0) return 0;
86
+
87
+ let shared = 0;
88
+ for (const [token, count] of leftCounts) {
89
+ shared += Math.min(count, rightCounts.get(token) ?? 0);
90
+ }
91
+ return (2 * shared) / (leftTotal + rightTotal);
92
+ }
93
+
94
+ describe('Locales must not copy another locale wholesale', () => {
95
+ ALL_ENTRIES.forEach((entry) => {
96
+ it(`${entry.id} is not at least ${COPY_THRESHOLD * 100}% identical to another locale`, async () => {
97
+ const corpora = new Map<string, string>();
98
+
99
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
100
+ if (!loader) continue;
101
+ corpora.set(locale, localeCorpus(await loader()));
102
+ }
103
+
104
+ const locales = [...corpora.keys()];
105
+ const violations: string[] = [];
106
+
107
+ for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
108
+ for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
109
+ const left = locales[leftIndex];
110
+ const right = locales[rightIndex];
111
+ const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
112
+
113
+ if (similarity >= COPY_THRESHOLD) {
114
+ violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
115
+ }
116
+ }
117
+ }
118
+
119
+ expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
120
+ });
121
+ });
122
+ });
123
+
124
+