@willwade/aac-processors 0.2.17 → 0.2.18

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.
@@ -333,6 +333,9 @@ class SnapProcessor extends BaseProcessor {
333
333
  buttonColumns.has('BackgroundColor') ? 'b.BackgroundColor' : 'NULL AS BackgroundColor',
334
334
  buttonColumns.has('NavigatePageId') ? 'b.NavigatePageId' : 'NULL AS NavigatePageId',
335
335
  buttonColumns.has('ContentType') ? 'b.ContentType' : 'NULL AS ContentType',
336
+ buttonColumns.has('SerializedContentTypeHandler')
337
+ ? 'b.SerializedContentTypeHandler'
338
+ : 'NULL AS SerializedContentTypeHandler',
336
339
  ];
337
340
  if (this.loadAudio) {
338
341
  selectFields.push(buttonColumns.has('MessageRecordingId')
@@ -548,22 +551,54 @@ class SnapProcessor extends BaseProcessor {
548
551
  },
549
552
  };
550
553
  }
554
+ let snapContentType;
555
+ let snapContentSubType;
556
+ let snapGrammarPos;
557
+ let snapGrammarHandler;
558
+ if (btnRow.ContentType === 1) {
559
+ snapContentType = 'AutoContent';
560
+ snapContentSubType = 'Prediction';
561
+ }
562
+ else if (btnRow.ContentType === 3 && btnRow.SerializedContentTypeHandler) {
563
+ snapContentType = 'Inflector';
564
+ snapGrammarHandler = String(btnRow.SerializedContentTypeHandler);
565
+ const colonIdx = snapGrammarHandler.indexOf(':');
566
+ if (colonIdx !== -1) {
567
+ const subtype = snapGrammarHandler.substring(colonIdx + 1).split(',')[0];
568
+ snapContentSubType = subtype;
569
+ const { TDSnapLexiconParser, } = require('../utilities/analytics/morphology/tdsnapLexiconParser');
570
+ snapGrammarPos = TDSnapLexiconParser.tagToPos(subtype);
571
+ }
572
+ }
551
573
  const button = new AACButton({
552
574
  id: String(btnRow.Id),
553
575
  label: btnRow.Label || (btnRow.ContentType === 1 ? '[Prediction]' : ''),
554
576
  message: btnRow.Message || (btnRow.ContentType === 1 ? '[Prediction]' : btnRow.Label || ''),
555
577
  targetPageId: targetPageUniqueId,
556
578
  semanticAction: semanticAction,
557
- contentType: btnRow.ContentType === 1 ? 'AutoContent' : undefined,
558
- contentSubType: btnRow.ContentType === 1 ? 'Prediction' : undefined,
579
+ contentType: snapContentType,
580
+ contentSubType: snapContentSubType,
559
581
  audioRecording: audioRecording,
560
582
  visibility: mapSnapVisibility(btnRow.Visible),
561
583
  semantic_id: btnRow.LibrarySymbolId
562
584
  ? `snap_symbol_${btnRow.LibrarySymbolId}`
563
- : undefined, // Extract semantic_id from LibrarySymbolId
585
+ : undefined,
564
586
  image: buttonImage,
565
587
  resolvedImageEntry: buttonImage,
566
- parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
588
+ parameters: {
589
+ ...(Object.keys(buttonParameters).length > 0 ? buttonParameters : {}),
590
+ ...(snapGrammarHandler
591
+ ? {
592
+ grammar: {
593
+ handler: snapGrammarHandler,
594
+ category: snapGrammarHandler.substring(0, snapGrammarHandler.indexOf(':') !== -1
595
+ ? snapGrammarHandler.indexOf(':')
596
+ : snapGrammarHandler.length),
597
+ subtype: snapContentSubType,
598
+ },
599
+ }
600
+ : {}),
601
+ },
567
602
  style: {
568
603
  backgroundColor: btnRow.BackgroundColor
569
604
  ? `#${btnRow.BackgroundColor.toString(16)}`
@@ -576,6 +611,9 @@ class SnapProcessor extends BaseProcessor {
576
611
  fontStyle: btnRow.FontStyle?.toString(),
577
612
  },
578
613
  });
614
+ if (snapGrammarPos) {
615
+ button.pos = snapGrammarPos;
616
+ }
579
617
  // Add to the intended parent page
580
618
  const parentPage = tree.getPage(parentUniqueId);
581
619
  if (parentPage) {
@@ -585,12 +585,20 @@ export class MetricsCalculator {
585
585
  /**
586
586
  * Quick check whether any button in the tree has a POS tag.
587
587
  * Used to auto-enable smart grammar without requiring explicit opt-in.
588
+ *
589
+ * IMPORTANT: Only counts POS from non-Inflector and non-Suffix buttons.
590
+ * TDSnap Inflector buttons and Grid3 Suffix buttons are grammar controls,
591
+ * not content words — they should NOT auto-enable morphology.
588
592
  */
589
593
  treeHasPosTags(tree) {
590
594
  for (const page of Object.values(tree.pages)) {
591
595
  for (const row of page.grid) {
592
596
  for (const btn of row) {
593
- if (btn?.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
597
+ if (btn?.pos &&
598
+ btn.pos !== 'Unknown' &&
599
+ btn.pos !== 'Ignore' &&
600
+ btn.pos !== 'Suffix' &&
601
+ btn.contentType !== 'Inflector') {
594
602
  return true;
595
603
  }
596
604
  }
@@ -608,8 +616,44 @@ export class MetricsCalculator {
608
616
  */
609
617
  expandMorphologicalPredictions(tree, options) {
610
618
  const locale = options.morphologyLocale || 'en-gb';
611
- const morph = new MorphologyEngine(locale);
612
- // Words that should never be POS-inferred (function words, determiners, etc.)
619
+ let morph;
620
+ if (options.tdsnapLexiconPath) {
621
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
622
+ const { TDSnapLexiconParser } = require('../morphology/tdsnapLexiconParser');
623
+ const parser = new TDSnapLexiconParser();
624
+ const lexiconData = parser.parseDb(options.tdsnapLexiconPath, locale.replace('-', '_'));
625
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
626
+ morph = MorphologyEngine.fromTDSnapLexicon(lexiconData);
627
+ this.expandTDSnapPredictions(tree, morph);
628
+ return;
629
+ }
630
+ if (options.grid3VerbsPath) {
631
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
632
+ const { Grid3VerbsParser } = require('../morphology/grid3VerbsParser');
633
+ const parser = new Grid3VerbsParser();
634
+ const verbForms = parser.parseZip(options.grid3VerbsPath);
635
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
636
+ morph = MorphologyEngine.fromGrid3Verbs(verbForms);
637
+ this.expandGrid3Predictions(tree, morph);
638
+ return;
639
+ }
640
+ morph = new MorphologyEngine(locale);
641
+ this.expandGrid3Predictions(tree, morph);
642
+ }
643
+ /**
644
+ * Expand morphological predictions for Grid3 pagesets.
645
+ *
646
+ * Grid3 uses suffix buttons (pos='Suffix') on the same page as content words.
647
+ * Different pages have different suffix buttons — e.g., topic pages may only
648
+ * have -s (plural), while the Magic Wand page has -s, -er, -est, -ly, -y, -'s.
649
+ *
650
+ * Rules:
651
+ * 1. Build a suffix→formSlot map (-s → plural, -er → comparative, etc.)
652
+ * 2. For each page, collect available suffix buttons
653
+ * 3. Only generate forms for slots that have matching suffix buttons on that page
654
+ * 4. POS inference is used for untagged content words (Grid3 grids often lack POS)
655
+ */
656
+ expandGrid3Predictions(tree, morph) {
613
657
  const skipInference = new Set([
614
658
  'a',
615
659
  'an',
@@ -692,30 +736,56 @@ export class MetricsCalculator {
692
736
  'wow',
693
737
  'sorry',
694
738
  ]);
739
+ // Map suffix button labels to the morphology slots they produce
740
+ const SUFFIX_TO_SLOT = {
741
+ '-s': ['plural'],
742
+ "-'s": ['possessive'],
743
+ '-er': ['comparative'],
744
+ '-est': ['superlative'],
745
+ '-ly': ['adverb'],
746
+ '-y': ['adjective'],
747
+ };
748
+ // POS → slots that POS can produce (for filtering)
749
+ const POS_TO_SUFFIX_SLOTS = {
750
+ Noun: new Set(['plural', 'possessive']),
751
+ Verb: new Set(['plural']),
752
+ Adjective: new Set(['comparative', 'superlative', 'adverb', 'adjective']),
753
+ };
695
754
  for (const page of Object.values(tree.pages)) {
755
+ // Collect suffix buttons on this page
756
+ const pageSuffixes = new Set();
757
+ const pageSuffixSlots = new Set();
758
+ for (const row of page.grid) {
759
+ for (const btn of row) {
760
+ if (btn?.pos === 'Suffix' && btn.label) {
761
+ pageSuffixes.add(btn.label);
762
+ const slots = SUFFIX_TO_SLOT[btn.label];
763
+ if (slots) {
764
+ for (const s of slots)
765
+ pageSuffixSlots.add(s);
766
+ }
767
+ }
768
+ }
769
+ }
770
+ // No suffix buttons on this page → no morphology
771
+ if (pageSuffixSlots.size === 0)
772
+ continue;
696
773
  for (const row of page.grid) {
697
774
  for (const btn of row) {
698
775
  if (!btn || !btn.label)
699
776
  continue;
777
+ if (btn.pos === 'Suffix')
778
+ continue;
700
779
  let pos = btn.pos;
701
- // If no POS tag (or Unknown/Ignore), attempt POS inference.
702
- // Many content words on topic pages lack POS tags even though
703
- // they are clearly nouns (e.g., "bird", "tree", "cloud").
704
- // Strategy: check irregular tables first for confident POS,
705
- // then fall back to Noun for single-word content labels.
706
780
  if (!pos || pos === 'Unknown' || pos === 'Ignore') {
707
781
  const lower = btn.label.toLowerCase();
708
- // Skip function words and multi-word labels
709
782
  if (!skipInference.has(lower) && !lower.includes(' ') && lower.length > 1) {
710
- // Check irregular tables for confident POS assignment
711
783
  const inferredPOS = morph.inferPOS(lower);
712
784
  if (inferredPOS) {
713
785
  pos = inferredPOS;
714
786
  btn.pos = inferredPOS;
715
787
  }
716
788
  else {
717
- // Default to Noun for untagged content words.
718
- // This generates plurals (e.g., bird → birds, tree → trees).
719
789
  pos = 'Noun';
720
790
  btn.pos = 'Noun';
721
791
  }
@@ -723,16 +793,90 @@ export class MetricsCalculator {
723
793
  }
724
794
  if (!pos || pos === 'Unknown' || pos === 'Ignore')
725
795
  continue;
726
- const forms = morph.inflect(btn.label, pos);
727
- if (forms.length > 0) {
796
+ // Check if this POS can produce forms matching the page's suffix slots
797
+ const posSlots = POS_TO_SUFFIX_SLOTS[pos];
798
+ if (!posSlots)
799
+ continue;
800
+ const hasRelevantSlot = [...posSlots].some((s) => pageSuffixSlots.has(s));
801
+ if (!hasRelevantSlot)
802
+ continue;
803
+ const allForms = morph.inflect(btn.label, pos);
804
+ if (allForms.length === 0)
805
+ continue;
806
+ // Filter forms: only include those producible by suffixes on this page
807
+ // For the built-in engine, we can't easily map forms to slots, so
808
+ // include all forms when any relevant suffix exists. The per-page
809
+ // gate (suffix presence) is the main filter.
810
+ const existing = btn.predictions || [];
811
+ const merged = new Set([...existing, ...allForms]);
812
+ btn.predictions = Array.from(merged);
813
+ }
814
+ }
815
+ }
816
+ }
817
+ /**
818
+ * Expand morphological predictions for TDSnap pagesets.
819
+ *
820
+ * TDSnap uses Inflector buttons (ContentType=3) on "Word Forms" pages to
821
+ * provide morphology. These pages are loaded dynamically by the runtime,
822
+ * NOT via navigation buttons, so they are unreachable in our tree model.
823
+ *
824
+ * Rules:
825
+ * 1. If the pageset has NO Inflector buttons → no morphology at all
826
+ * 2. Only generate forms whose grammar tag matches an available Inflector
827
+ * (e.g., if there's no -ly Inflector, don't generate "happily")
828
+ * 3. No POS inference — only the lexicon determines which words get forms
829
+ */
830
+ expandTDSnapPredictions(tree, morph) {
831
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
832
+ const { TDSnapLexiconParser } = require('../morphology/tdsnapLexiconParser');
833
+ // Step 1: Collect available grammar tags from Inflector buttons
834
+ const availableTags = new Set();
835
+ for (const page of Object.values(tree.pages)) {
836
+ for (const row of page.grid) {
837
+ for (const btn of row) {
838
+ if (btn?.contentType === 'Inflector' && btn.parameters?.grammar?.handler) {
839
+ const parsed = TDSnapLexiconParser.parseContentTypeHandler(btn.parameters.grammar.handler);
840
+ if (parsed) {
841
+ const key = `${parsed.category}:${parsed.subtype}`;
842
+ const tag = TDSnapLexiconParser.HANDLER_TAG_MAP[key];
843
+ if (tag)
844
+ availableTags.add(tag);
845
+ }
846
+ }
847
+ }
848
+ }
849
+ }
850
+ if (availableTags.size === 0)
851
+ return;
852
+ // Step 2: For each button, look up lexicon forms filtered by available tags
853
+ for (const page of Object.values(tree.pages)) {
854
+ for (const row of page.grid) {
855
+ for (const btn of row) {
856
+ if (!btn || !btn.label || btn.contentType === 'Inflector')
857
+ continue;
858
+ const filtered = this.filterFormsByAvailableTags(morph, btn.label, availableTags);
859
+ if (filtered.length > 0) {
728
860
  const existing = btn.predictions || [];
729
- const merged = new Set([...existing, ...forms]);
861
+ const merged = new Set([...existing, ...filtered]);
730
862
  btn.predictions = Array.from(merged);
731
863
  }
732
864
  }
733
865
  }
734
866
  }
735
867
  }
868
+ filterFormsByAvailableTags(morph, base, availableTags) {
869
+ const entry = morph.getLexiconEntry(base.toLowerCase());
870
+ if (!entry)
871
+ return [];
872
+ const forms = [];
873
+ for (const f of entry.forms) {
874
+ if (availableTags.has(f.tag) && f.form.toLowerCase() !== base.toLowerCase()) {
875
+ forms.push(f.form);
876
+ }
877
+ }
878
+ return forms;
879
+ }
736
880
  /**
737
881
  * Calculate metrics for word forms (smart grammar predictions)
738
882
  *
@@ -749,7 +893,7 @@ export class MetricsCalculator {
749
893
  * @param options - Metrics options
750
894
  * @returns Object containing word form metrics and labels that were replaced
751
895
  */
752
- calculateWordFormMetrics(tree, buttons, _options = {}) {
896
+ calculateWordFormMetrics(tree, buttons, options = {}) {
753
897
  const wordFormMetrics = [];
754
898
  const replacedLabels = new Set();
755
899
  // Track buttons by label to compare efforts
@@ -811,23 +955,28 @@ export class MetricsCalculator {
811
955
  // Calculate effort for each word form
812
956
  btn.predictions.forEach((wordForm, index) => {
813
957
  const wordFormLower = wordForm.toLowerCase();
814
- // Calculate effort based on position in predictions array
815
- // Assume predictions are displayed in a grid layout (e.g., 2 columns)
816
- const predictionsGridCols = 2; // Typical predictions layout
817
- const predictionRowIndex = Math.floor(index / predictionsGridCols);
818
- const predictionColIndex = index % predictionsGridCols;
819
- // Calculate visual scan effort to reach this word form position
820
- // Using similar logic to button scanning effort
821
- const predictionPriorItems = predictionRowIndex * predictionsGridCols + predictionColIndex;
822
- const predictionSelectionEffort = visualScanEffort(predictionPriorItems);
823
- // Add confirmation cost for Suggest Words outcomes only.
824
- // Suggest Words requires an explicit tap on the prediction bar,
825
- // while smart grammar morphology forms are auto-generated (no extra tap).
826
- const suggestWordsConfirmation = suggestWordsSet.has(wordFormLower)
827
- ? EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT
828
- : 0;
829
- // Word form effort = parent button's cumulative effort + selection effort + confirmation
830
- const wordFormEffort = parentMetrics.effort + predictionSelectionEffort + suggestWordsConfirmation;
958
+ const isSuggestWords = suggestWordsSet.has(wordFormLower);
959
+ let wordFormEffort;
960
+ if (options.tdsnapLexiconPath && !isSuggestWords) {
961
+ // TDSnap Inflector-based form: the grammar overlay appears
962
+ // dynamically when a word is selected. The cost is a fixed
963
+ // single selection to tap the Inflector button.
964
+ wordFormEffort =
965
+ parentMetrics.effort + EFFORT_CONSTANTS.TDSNAP_GRAMMAR_OVERLAY_EFFORT;
966
+ }
967
+ else {
968
+ // Grid-based prediction layout (Suggest Words, or Grid3 morphology)
969
+ const predictionsGridCols = 2;
970
+ const predictionRowIndex = Math.floor(index / predictionsGridCols);
971
+ const predictionColIndex = index % predictionsGridCols;
972
+ const predictionPriorItems = predictionRowIndex * predictionsGridCols + predictionColIndex;
973
+ const predictionSelectionEffort = visualScanEffort(predictionPriorItems);
974
+ const suggestWordsConfirmation = isSuggestWords
975
+ ? EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT
976
+ : 0;
977
+ wordFormEffort =
978
+ parentMetrics.effort + predictionSelectionEffort + suggestWordsConfirmation;
979
+ }
831
980
  // Check if this word already exists as a regular button
832
981
  const existingBtn = existingLabels.get(wordFormLower);
833
982
  // If word exists and has lower or equal effort, skip the word form
@@ -32,6 +32,7 @@ export const EFFORT_CONSTANTS = {
32
32
  DEFAULT_SCAN_ERROR_RATE: 0.1, // 10% chance of missing a selection
33
33
  SCAN_RETRY_PENALTY: 1.0, // Cost multiplier for a full loop retry
34
34
  SUGGEST_WORDS_SELECTION_EFFORT: 0.5, // Extra tap to confirm a Suggest Words prediction
35
+ TDSNAP_GRAMMAR_OVERLAY_EFFORT: 0.1, // Fixed cost: select an Inflector button from the dynamic grammar overlay
35
36
  };
36
37
  /**
37
38
  * Calculate button size effort based on grid dimensions
@@ -1,4 +1,7 @@
1
1
  export class MorphologyEngine {
2
+ getLexiconEntry(word) {
3
+ return this.tdsnapLexicon?.words.get(word.toLowerCase());
4
+ }
2
5
  constructor(ruleSetOrLocale) {
3
6
  this.cache = new Map();
4
7
  if (typeof ruleSetOrLocale === 'string') {
@@ -18,6 +21,16 @@ export class MorphologyEngine {
18
21
  engine.grid3Verbs = verbForms.verbs;
19
22
  return engine;
20
23
  }
24
+ static fromTDSnapLexicon(lexiconData) {
25
+ const engine = new MorphologyEngine({
26
+ locale: lexiconData.locale,
27
+ version: 1,
28
+ irregular: {},
29
+ regular: {},
30
+ });
31
+ engine.tdsnapLexicon = lexiconData;
32
+ return engine;
33
+ }
21
34
  get locale() {
22
35
  return this.ruleSet.locale;
23
36
  }
@@ -26,9 +39,18 @@ export class MorphologyEngine {
26
39
  const cached = this.cache.get(key);
27
40
  if (cached)
28
41
  return cached;
42
+ if (this.tdsnapLexicon) {
43
+ const entry = this.tdsnapLexicon.words.get(base.toLowerCase());
44
+ if (entry) {
45
+ const forms = entry.forms.map((f) => f.form);
46
+ this.cache.set(key, forms);
47
+ return forms;
48
+ }
49
+ }
29
50
  if (this.grid3Verbs) {
30
- const forms = this.grid3Verbs.get(base) || this.grid3Verbs.get(base.toLowerCase());
31
- if (forms) {
51
+ const raw = this.grid3Verbs.get(base) || this.grid3Verbs.get(base.toLowerCase());
52
+ if (raw) {
53
+ const forms = raw.filter((f) => !f.includes('{'));
32
54
  this.cache.set(key, forms);
33
55
  return forms;
34
56
  }
@@ -1,2 +1,3 @@
1
1
  export { MorphologyEngine } from './engine';
2
2
  export { WordFormGenerator } from './wordFormGenerator';
3
+ export { TDSnapLexiconParser } from './tdsnapLexiconParser';
@@ -0,0 +1,182 @@
1
+ export class TDSnapLexiconParser {
2
+ parseDb(dbPath, locale) {
3
+ const detectedLocale = locale || this.inferLocale(dbPath);
4
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
5
+ const Database = require('better-sqlite3');
6
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
7
+ const db = new Database(dbPath, { readonly: true });
8
+ try {
9
+ return this.extractAll(db, detectedLocale);
10
+ }
11
+ finally {
12
+ db.close();
13
+ }
14
+ }
15
+ inferLocale(dbPath) {
16
+ const match = dbPath.match(/lang_([a-z]{2}_[A-Z]{2})/i);
17
+ return match ? match[1] : 'unknown';
18
+ }
19
+ extractAll(db, locale) {
20
+ const words = new Map();
21
+ const subclassCache = new Map();
22
+ const getSubclass = (id) => {
23
+ let name = subclassCache.get(id);
24
+ if (name !== undefined)
25
+ return name;
26
+ const row = db.prepare('SELECT Name FROM PosSubclass WHERE Id = ?').get(id);
27
+ name = row?.Name;
28
+ if (name) {
29
+ subclassCache.set(id, name);
30
+ return name;
31
+ }
32
+ return undefined;
33
+ };
34
+ const allWords = db
35
+ .prepare(`SELECT w.Id as wordId, w.Text as text,
36
+ i.Id as inflectionId, i.LexemeId as lexemeId, i.PosSubclassId as posSubclassId
37
+ FROM Word w
38
+ JOIN Spelling s ON s.WordId = w.Id
39
+ JOIN Inflection i ON i.Id = s.InflectionId
40
+ WHERE i.PosSubclassId != 0
41
+ ORDER BY w.Text`)
42
+ .all();
43
+ const lexemeForms = new Map();
44
+ for (const row of allWords) {
45
+ const tag = getSubclass(row.posSubclassId);
46
+ if (!tag)
47
+ continue;
48
+ let formsByTag = lexemeForms.get(row.lexemeId);
49
+ if (!formsByTag) {
50
+ formsByTag = new Map();
51
+ lexemeForms.set(row.lexemeId, formsByTag);
52
+ }
53
+ const existing = formsByTag.get(tag);
54
+ if (existing) {
55
+ if (!existing.includes(row.text))
56
+ existing.push(row.text);
57
+ }
58
+ else {
59
+ formsByTag.set(tag, [row.text]);
60
+ }
61
+ }
62
+ const wordToLexeme = new Map();
63
+ for (const row of allWords) {
64
+ if (!wordToLexeme.has(row.text.toLowerCase())) {
65
+ wordToLexeme.set(row.text.toLowerCase(), row.lexemeId);
66
+ }
67
+ }
68
+ for (const [text, lexemeId] of wordToLexeme) {
69
+ const formsByTag = lexemeForms.get(lexemeId);
70
+ if (!formsByTag || formsByTag.size === 0)
71
+ continue;
72
+ const forms = [];
73
+ for (const [tag, formTexts] of formsByTag) {
74
+ for (const formText of formTexts) {
75
+ if (formText.toLowerCase() !== text) {
76
+ forms.push({ tag, form: formText });
77
+ }
78
+ }
79
+ }
80
+ if (forms.length > 0) {
81
+ words.set(text, { lexemeId, forms });
82
+ }
83
+ }
84
+ return { locale, words };
85
+ }
86
+ lookupWord(data, word) {
87
+ const entry = data.words.get(word.toLowerCase());
88
+ if (!entry)
89
+ return [];
90
+ return entry.forms.map((f) => f.form);
91
+ }
92
+ lookupWordByTag(data, word, tag) {
93
+ const entry = data.words.get(word.toLowerCase());
94
+ if (!entry)
95
+ return [];
96
+ return entry.forms.filter((f) => f.tag === tag).map((f) => f.form);
97
+ }
98
+ static parseContentTypeHandler(handler) {
99
+ if (!handler)
100
+ return null;
101
+ const colonIdx = handler.indexOf(':');
102
+ if (colonIdx === -1) {
103
+ const parts = handler.split(',');
104
+ return { category: parts[0], subtype: '', params: parts.slice(1) };
105
+ }
106
+ const category = handler.substring(0, colonIdx);
107
+ const rest = handler.substring(colonIdx + 1);
108
+ const commaIdx = rest.indexOf(',');
109
+ if (commaIdx === -1) {
110
+ return { category, subtype: rest, params: [] };
111
+ }
112
+ const subtype = rest.substring(0, commaIdx);
113
+ const paramsStr = rest.substring(commaIdx + 1);
114
+ const params = paramsStr.split(',').map((p) => p.trim());
115
+ return { category, subtype, params };
116
+ }
117
+ static tagToPos(tag) {
118
+ return TDSnapLexiconParser.TAG_TO_POS[tag] || 'Unknown';
119
+ }
120
+ static handlerToPos(handler) {
121
+ const parsed = TDSnapLexiconParser.parseContentTypeHandler(handler);
122
+ if (!parsed)
123
+ return 'Unknown';
124
+ if (parsed.category === 'RESET' || parsed.category === 'SPECIAL')
125
+ return 'Ignore';
126
+ const key = `${parsed.category}:${parsed.subtype}`;
127
+ const tag = TDSnapLexiconParser.HANDLER_TAG_MAP[key];
128
+ if (tag)
129
+ return TDSnapLexiconParser.TAG_TO_POS[tag] || 'Unknown';
130
+ return TDSnapLexiconParser.TAG_TO_POS[parsed.subtype] || 'Unknown';
131
+ }
132
+ }
133
+ TDSnapLexiconParser.TAG_TO_POS = {
134
+ V0: 'Verb',
135
+ VZ: 'Verb',
136
+ VG: 'Verb',
137
+ VD: 'Verb',
138
+ VN: 'Verb',
139
+ SNG: 'Noun',
140
+ PLU: 'Noun',
141
+ ADJ: 'Adjective',
142
+ ADJR: 'Adjective',
143
+ ADJT: 'Adjective',
144
+ ADV: 'Adjective',
145
+ SUB: 'Pronoun',
146
+ OBJ: 'Pronoun',
147
+ POS: 'Pronoun',
148
+ NPOS: 'Pronoun',
149
+ REF: 'Pronoun',
150
+ B0: 'Verb',
151
+ BZ: 'Verb',
152
+ BM: 'Verb',
153
+ BR: 'Verb',
154
+ BDZ: 'Verb',
155
+ BDR: 'Verb',
156
+ BG: 'Verb',
157
+ BN: 'Verb',
158
+ };
159
+ TDSnapLexiconParser.HANDLER_TAG_MAP = {
160
+ 'NOUN:PLU': 'PLU',
161
+ 'DESCRIBE:ADJR': 'ADJR',
162
+ 'DESCRIBE:ADJT': 'ADJT',
163
+ 'DESCRIBE:ADV': 'ADV',
164
+ 'VERB:V0': 'V0',
165
+ 'VERB:VZ': 'VZ',
166
+ 'VERB:VG': 'VG',
167
+ 'VERB:VD': 'VD',
168
+ 'VERB:VN': 'VN',
169
+ 'PRONOUN:SUB': 'SUB',
170
+ 'PRONOUN:OBJ': 'OBJ',
171
+ 'PRONOUN:POS': 'POS',
172
+ 'PRONOUN:NPOS': 'NPOS',
173
+ 'PRONOUN:REF': 'REF',
174
+ 'BE:B0': 'B0',
175
+ 'BE:BZ': 'BZ',
176
+ 'BE:BM': 'BM',
177
+ 'BE:BR': 'BR',
178
+ 'BE:BDZ': 'BDZ',
179
+ 'BE:BDR': 'BDR',
180
+ 'BE:BG': 'BG',
181
+ 'BE:BN': 'BN',
182
+ };
@@ -121,7 +121,7 @@ export declare class AACButton {
121
121
  identifier?: string;
122
122
  metadata?: string;
123
123
  };
124
- contentType?: 'Normal' | 'AutoContent' | 'Workspace' | 'LiveCell';
124
+ contentType?: 'Normal' | 'AutoContent' | 'Workspace' | 'LiveCell' | 'Inflector' | 'Prediction';
125
125
  contentSubType?: string;
126
126
  image?: string;
127
127
  resolvedImageEntry?: string;
@@ -169,7 +169,7 @@ export declare class AACButton {
169
169
  metadata?: string;
170
170
  };
171
171
  style?: AACStyle;
172
- contentType?: 'Normal' | 'AutoContent' | 'Workspace' | 'LiveCell';
172
+ contentType?: 'Normal' | 'AutoContent' | 'Workspace' | 'LiveCell' | 'Inflector' | 'Prediction';
173
173
  contentSubType?: string;
174
174
  image?: string;
175
175
  resolvedImageEntry?: string;
@@ -13,6 +13,7 @@ export * as Analytics from './analytics';
13
13
  export * as Validation from './validation';
14
14
  export * as Metrics from './metrics';
15
15
  export { Grid3VerbsParser } from './utilities/analytics/morphology/grid3VerbsParser';
16
+ export { TDSnapLexiconParser } from './utilities/analytics/morphology/tdsnapLexiconParser';
16
17
  export { WordFormGenerator } from './utilities/analytics/morphology/wordFormGenerator';
17
18
  export * as Gridset from './gridset';
18
19
  export * as Snap from './snap';
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
33
33
  return result;
34
34
  };
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Translation = exports.AstericsGrid = exports.ApplePanels = exports.Opml = exports.Excel = exports.Dot = exports.TouchChat = exports.Obfset = exports.OBF = exports.Snap = exports.Gridset = exports.WordFormGenerator = exports.Grid3VerbsParser = exports.Metrics = exports.Validation = exports.Analytics = void 0;
36
+ exports.Translation = exports.AstericsGrid = exports.ApplePanels = exports.Opml = exports.Excel = exports.Dot = exports.TouchChat = exports.Obfset = exports.OBF = exports.Snap = exports.Gridset = exports.WordFormGenerator = exports.TDSnapLexiconParser = exports.Grid3VerbsParser = exports.Metrics = exports.Validation = exports.Analytics = void 0;
37
37
  exports.getProcessor = getProcessor;
38
38
  exports.getSupportedExtensions = getSupportedExtensions;
39
39
  exports.isExtensionSupported = isExtensionSupported;
@@ -56,9 +56,11 @@ exports.Analytics = __importStar(require("./analytics"));
56
56
  exports.Validation = __importStar(require("./validation"));
57
57
  // Metrics namespace (pageset analytics)
58
58
  exports.Metrics = __importStar(require("./metrics"));
59
- // Node-only morphology utilities (Grid 3 verbs parser)
59
+ // Node-only morphology utilities (Grid 3 verbs parser, TDSnap lexicon parser)
60
60
  var grid3VerbsParser_1 = require("./utilities/analytics/morphology/grid3VerbsParser");
61
61
  Object.defineProperty(exports, "Grid3VerbsParser", { enumerable: true, get: function () { return grid3VerbsParser_1.Grid3VerbsParser; } });
62
+ var tdsnapLexiconParser_1 = require("./utilities/analytics/morphology/tdsnapLexiconParser");
63
+ Object.defineProperty(exports, "TDSnapLexiconParser", { enumerable: true, get: function () { return tdsnapLexiconParser_1.TDSnapLexiconParser; } });
62
64
  var wordFormGenerator_1 = require("./utilities/analytics/morphology/wordFormGenerator");
63
65
  Object.defineProperty(exports, "WordFormGenerator", { enumerable: true, get: function () { return wordFormGenerator_1.WordFormGenerator; } });
64
66
  // Processor namespaces (platform-specific utilities)