@willwade/aac-processors 0.2.3 → 0.2.5
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/browser/core/treeStructure.js +3 -1
- package/dist/browser/metrics.js +2 -0
- package/dist/browser/processors/astericsGridProcessor.js +21 -12
- package/dist/browser/processors/gridset/helpers.js +3 -3
- package/dist/browser/processors/gridsetProcessor.js +269 -239
- package/dist/browser/processors/obfProcessor.js +4 -4
- package/dist/browser/processors/snap/helpers.js +3 -3
- package/dist/browser/processors/snapProcessor.js +2 -2
- package/dist/browser/processors/touchchatProcessor.js +6 -6
- package/dist/browser/utilities/analytics/metrics/core.js +213 -8
- package/dist/browser/utilities/analytics/metrics/vocabulary.js +13 -1
- package/dist/browser/utilities/analytics/morphology/engine.js +910 -0
- package/dist/browser/utilities/analytics/morphology/grid3VerbsParser.js +455 -0
- package/dist/browser/utilities/analytics/morphology/index.js +3 -0
- package/dist/browser/utilities/analytics/morphology/types.js +1 -0
- package/dist/browser/utilities/analytics/morphology/wordFormGenerator.js +74 -0
- package/dist/core/treeStructure.d.ts +17 -1
- package/dist/core/treeStructure.js +3 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +6 -1
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +5 -1
- package/dist/processors/astericsGridProcessor.js +21 -12
- package/dist/processors/excelProcessor.js +5 -1
- package/dist/processors/gridset/helpers.js +3 -3
- package/dist/processors/gridset/imageDebug.js +2 -2
- package/dist/processors/gridsetProcessor.js +269 -239
- package/dist/processors/obfProcessor.js +4 -4
- package/dist/processors/snap/helpers.js +3 -3
- package/dist/processors/snapProcessor.js +2 -2
- package/dist/processors/touchchatProcessor.js +6 -6
- package/dist/utilities/analytics/metrics/core.d.ts +14 -0
- package/dist/utilities/analytics/metrics/core.js +213 -8
- package/dist/utilities/analytics/metrics/types.d.ts +18 -3
- package/dist/utilities/analytics/metrics/vocabulary.d.ts +3 -0
- package/dist/utilities/analytics/metrics/vocabulary.js +13 -1
- package/dist/utilities/analytics/morphology/engine.d.ts +30 -0
- package/dist/utilities/analytics/morphology/engine.js +914 -0
- package/dist/utilities/analytics/morphology/grid3VerbsParser.d.ts +36 -0
- package/dist/utilities/analytics/morphology/grid3VerbsParser.js +485 -0
- package/dist/utilities/analytics/morphology/index.d.ts +5 -0
- package/dist/utilities/analytics/morphology/index.js +9 -0
- package/dist/utilities/analytics/morphology/types.d.ts +40 -0
- package/dist/utilities/analytics/morphology/types.js +2 -0
- package/dist/utilities/analytics/morphology/wordFormGenerator.d.ts +10 -0
- package/dist/utilities/analytics/morphology/wordFormGenerator.js +78 -0
- package/package.json +12 -11
|
@@ -48,7 +48,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
catch (
|
|
51
|
+
catch (_err) {
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -94,7 +94,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
94
94
|
return dataUrl;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
catch (
|
|
97
|
+
catch (_err) {
|
|
98
98
|
// Continue to next path
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
@@ -318,7 +318,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
318
318
|
return obj;
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
catch (
|
|
321
|
+
catch (_error) {
|
|
322
322
|
// Log parsing errors for debugging but don't throw
|
|
323
323
|
}
|
|
324
324
|
return null;
|
|
@@ -725,7 +725,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
725
725
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-return
|
|
726
726
|
return require('../validation/obfValidator').ObfValidator;
|
|
727
727
|
}
|
|
728
|
-
catch (
|
|
728
|
+
catch (_error) {
|
|
729
729
|
throw new Error('Validation utilities are not available in this environment.');
|
|
730
730
|
}
|
|
731
731
|
}
|
|
@@ -24,7 +24,7 @@ async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFi
|
|
|
24
24
|
try {
|
|
25
25
|
entries = await listDir(current.dir);
|
|
26
26
|
}
|
|
27
|
-
catch (
|
|
27
|
+
catch (_error) {
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
for (const entry of entries) {
|
|
@@ -130,7 +130,7 @@ export async function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAd
|
|
|
130
130
|
const dir = dirname(dbPath);
|
|
131
131
|
await removePath(dir);
|
|
132
132
|
}
|
|
133
|
-
catch (
|
|
133
|
+
catch (_e) {
|
|
134
134
|
// Ignore cleanup errors
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -174,7 +174,7 @@ export async function findSnapPackages(packageNamePattern = 'TobiiDynavox', file
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
catch (
|
|
177
|
+
catch (_error) {
|
|
178
178
|
// Silently fail if directory access fails
|
|
179
179
|
}
|
|
180
180
|
return results;
|
|
@@ -215,7 +215,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
215
215
|
try {
|
|
216
216
|
positions = JSON.parse(sg.SerializedGridPositions);
|
|
217
217
|
}
|
|
218
|
-
catch (
|
|
218
|
+
catch (_e) {
|
|
219
219
|
// Invalid JSON, skip this group
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
@@ -436,7 +436,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
|
-
catch (
|
|
439
|
+
catch (_e) {
|
|
440
440
|
// Ignore JSON parse errors in commands
|
|
441
441
|
}
|
|
442
442
|
}
|
|
@@ -101,7 +101,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
101
101
|
idMappings.set(mapping.numeric_id, mapping.string_id);
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
-
catch (
|
|
104
|
+
catch (_e) {
|
|
105
105
|
// No mapping table, use numeric IDs as strings
|
|
106
106
|
}
|
|
107
107
|
// Load styles
|
|
@@ -119,7 +119,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
119
119
|
pageStyles.set(style.id, style);
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
catch (
|
|
122
|
+
catch (_e) {
|
|
123
123
|
// console.log('No styles found:', e);
|
|
124
124
|
}
|
|
125
125
|
// First, load all pages and get their names from resources
|
|
@@ -307,7 +307,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
307
307
|
}
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
|
-
catch (
|
|
310
|
+
catch (_e) {
|
|
311
311
|
// console.log('No button box cells found:', e);
|
|
312
312
|
}
|
|
313
313
|
// Load buttons directly linked to pages via resources
|
|
@@ -366,7 +366,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
366
366
|
page.addButton(button);
|
|
367
367
|
});
|
|
368
368
|
}
|
|
369
|
-
catch (
|
|
369
|
+
catch (_e) {
|
|
370
370
|
// console.log('No direct page buttons found:', e);
|
|
371
371
|
}
|
|
372
372
|
// Load navigation actions
|
|
@@ -413,7 +413,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
413
413
|
}
|
|
414
414
|
});
|
|
415
415
|
}
|
|
416
|
-
catch (
|
|
416
|
+
catch (_e) {
|
|
417
417
|
// console.log('No navigation actions found:', e);
|
|
418
418
|
}
|
|
419
419
|
// Try to load root ID from multiple sources in order of priority
|
|
@@ -454,7 +454,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
454
454
|
tree.metadata.defaultHomePageId = rootPageId;
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
|
-
catch (
|
|
457
|
+
catch (_e) {
|
|
458
458
|
// No metadata table or other error, use first page as root
|
|
459
459
|
if (rootPageId) {
|
|
460
460
|
tree.rootId = rootPageId;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { AACSemanticCategory, AACScanType, } from '../../../core/treeStructure';
|
|
10
10
|
import { CellScanningOrder, ScanningSelectionMethod } from '../../../types/aac';
|
|
11
11
|
import { baseBoardEffort, distanceEffort, visualScanEffort, EFFORT_CONSTANTS, localScanEffort, scanningEffort, } from './effort';
|
|
12
|
+
import { MorphologyEngine } from '../morphology';
|
|
12
13
|
export class MetricsCalculator {
|
|
13
14
|
constructor() {
|
|
14
15
|
this.locale = 'en';
|
|
@@ -80,9 +81,11 @@ export class MetricsCalculator {
|
|
|
80
81
|
});
|
|
81
82
|
// Update buttons using dynamic spelling effort if applicable
|
|
82
83
|
const buttons = Array.from(knownButtons.values()).sort((a, b) => a.effort - b.effort);
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Expand morphological predictions from POS tags if enabled or auto-detected
|
|
85
|
+
const useSmartGrammar = options.useSmartGrammar === true || this.treeHasPosTags(tree);
|
|
86
|
+
if (useSmartGrammar) {
|
|
87
|
+
this.expandMorphologicalPredictions(tree, options);
|
|
88
|
+
}
|
|
86
89
|
if (useSmartGrammar) {
|
|
87
90
|
const { wordFormMetrics, replacedLabels } = this.calculateWordFormMetrics(tree, buttons, options);
|
|
88
91
|
// Remove buttons that were replaced by lower-effort word forms
|
|
@@ -151,13 +154,21 @@ export class MetricsCalculator {
|
|
|
151
154
|
}) || null;
|
|
152
155
|
}
|
|
153
156
|
if (!spellingPage)
|
|
154
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
spellingPage: null,
|
|
159
|
+
spellingBaseEffort: 10,
|
|
160
|
+
spellingAvgLetterEffort: 2.5,
|
|
161
|
+
};
|
|
155
162
|
// Calculate effort to reach this page from root
|
|
156
163
|
const rootBoard = tree.rootId
|
|
157
164
|
? tree.pages[tree.rootId]
|
|
158
165
|
: Object.values(tree.pages).find((p) => !p.parentId);
|
|
159
166
|
if (!rootBoard)
|
|
160
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
spellingPage,
|
|
169
|
+
spellingBaseEffort: 10,
|
|
170
|
+
spellingAvgLetterEffort: 2.5,
|
|
171
|
+
};
|
|
161
172
|
// Analyze specifically to find the lowest effort path to the spelling page
|
|
162
173
|
const result = this.analyzeFrom(tree, rootBoard, setPcts, true, options);
|
|
163
174
|
const spellingBaseEffort = result.visitedBoardEfforts.get(spellingPage.id) ?? 10;
|
|
@@ -571,6 +582,157 @@ export class MetricsCalculator {
|
|
|
571
582
|
boardPcts['all'] = totalLinks;
|
|
572
583
|
return boardPcts;
|
|
573
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Quick check whether any button in the tree has a POS tag.
|
|
587
|
+
* Used to auto-enable smart grammar without requiring explicit opt-in.
|
|
588
|
+
*/
|
|
589
|
+
treeHasPosTags(tree) {
|
|
590
|
+
for (const page of Object.values(tree.pages)) {
|
|
591
|
+
for (const row of page.grid) {
|
|
592
|
+
for (const btn of row) {
|
|
593
|
+
if (btn?.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Expand morphological predictions from POS tags on buttons
|
|
603
|
+
*
|
|
604
|
+
* For each button that has a POS tag (e.g., 'Verb', 'Noun'), use the
|
|
605
|
+
* MorphologyEngine to generate inflected word forms and populate the
|
|
606
|
+
* button's predictions array. This is done as a pre-processing step
|
|
607
|
+
* before calculateWordFormMetrics assigns effort to each form.
|
|
608
|
+
*/
|
|
609
|
+
expandMorphologicalPredictions(tree, options) {
|
|
610
|
+
const locale = options.morphologyLocale || 'en-gb';
|
|
611
|
+
const morph = new MorphologyEngine(locale);
|
|
612
|
+
// Words that should never be POS-inferred (function words, determiners, etc.)
|
|
613
|
+
const skipInference = new Set([
|
|
614
|
+
'a',
|
|
615
|
+
'an',
|
|
616
|
+
'the',
|
|
617
|
+
'to',
|
|
618
|
+
'in',
|
|
619
|
+
'on',
|
|
620
|
+
'at',
|
|
621
|
+
'of',
|
|
622
|
+
'for',
|
|
623
|
+
'and',
|
|
624
|
+
'or',
|
|
625
|
+
'but',
|
|
626
|
+
'not',
|
|
627
|
+
'no',
|
|
628
|
+
'yes',
|
|
629
|
+
'is',
|
|
630
|
+
'am',
|
|
631
|
+
'are',
|
|
632
|
+
'was',
|
|
633
|
+
'were',
|
|
634
|
+
'be',
|
|
635
|
+
'been',
|
|
636
|
+
'being',
|
|
637
|
+
'has',
|
|
638
|
+
'have',
|
|
639
|
+
'had',
|
|
640
|
+
'do',
|
|
641
|
+
'does',
|
|
642
|
+
'did',
|
|
643
|
+
'will',
|
|
644
|
+
'would',
|
|
645
|
+
'could',
|
|
646
|
+
'should',
|
|
647
|
+
'shall',
|
|
648
|
+
'may',
|
|
649
|
+
'might',
|
|
650
|
+
'can',
|
|
651
|
+
'must',
|
|
652
|
+
'with',
|
|
653
|
+
'from',
|
|
654
|
+
'by',
|
|
655
|
+
'up',
|
|
656
|
+
'down',
|
|
657
|
+
'out',
|
|
658
|
+
'off',
|
|
659
|
+
'over',
|
|
660
|
+
'under',
|
|
661
|
+
'again',
|
|
662
|
+
'then',
|
|
663
|
+
'than',
|
|
664
|
+
'so',
|
|
665
|
+
'if',
|
|
666
|
+
'when',
|
|
667
|
+
'where',
|
|
668
|
+
'how',
|
|
669
|
+
'what',
|
|
670
|
+
'who',
|
|
671
|
+
'which',
|
|
672
|
+
'that',
|
|
673
|
+
'this',
|
|
674
|
+
'these',
|
|
675
|
+
'those',
|
|
676
|
+
'here',
|
|
677
|
+
'there',
|
|
678
|
+
'now',
|
|
679
|
+
'very',
|
|
680
|
+
'just',
|
|
681
|
+
'more',
|
|
682
|
+
'also',
|
|
683
|
+
'too',
|
|
684
|
+
'please',
|
|
685
|
+
'thank',
|
|
686
|
+
'hi',
|
|
687
|
+
'hello',
|
|
688
|
+
'bye',
|
|
689
|
+
'goodbye',
|
|
690
|
+
'okay',
|
|
691
|
+
'oh',
|
|
692
|
+
'wow',
|
|
693
|
+
'sorry',
|
|
694
|
+
]);
|
|
695
|
+
for (const page of Object.values(tree.pages)) {
|
|
696
|
+
for (const row of page.grid) {
|
|
697
|
+
for (const btn of row) {
|
|
698
|
+
if (!btn || !btn.label)
|
|
699
|
+
continue;
|
|
700
|
+
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
|
+
if (!pos || pos === 'Unknown' || pos === 'Ignore') {
|
|
707
|
+
const lower = btn.label.toLowerCase();
|
|
708
|
+
// Skip function words and multi-word labels
|
|
709
|
+
if (!skipInference.has(lower) && !lower.includes(' ') && lower.length > 1) {
|
|
710
|
+
// Check irregular tables for confident POS assignment
|
|
711
|
+
const inferredPOS = morph.inferPOS(lower);
|
|
712
|
+
if (inferredPOS) {
|
|
713
|
+
pos = inferredPOS;
|
|
714
|
+
btn.pos = inferredPOS;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
// Default to Noun for untagged content words.
|
|
718
|
+
// This generates plurals (e.g., bird → birds, tree → trees).
|
|
719
|
+
pos = 'Noun';
|
|
720
|
+
btn.pos = 'Noun';
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (!pos || pos === 'Unknown' || pos === 'Ignore')
|
|
725
|
+
continue;
|
|
726
|
+
const forms = morph.inflect(btn.label, pos);
|
|
727
|
+
if (forms.length > 0) {
|
|
728
|
+
const existing = btn.predictions || [];
|
|
729
|
+
const merged = new Set([...existing, ...forms]);
|
|
730
|
+
btn.predictions = Array.from(merged);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
574
736
|
/**
|
|
575
737
|
* Calculate metrics for word forms (smart grammar predictions)
|
|
576
738
|
*
|
|
@@ -593,14 +755,53 @@ export class MetricsCalculator {
|
|
|
593
755
|
// Track buttons by label to compare efforts
|
|
594
756
|
const existingLabels = new Map();
|
|
595
757
|
buttons.forEach((btn) => existingLabels.set(btn.label.toLowerCase(), btn));
|
|
758
|
+
// Build a map of POS tags from ALL tree buttons, keyed by lowercase label.
|
|
759
|
+
// This ensures words on BFS-unreachable pages still contribute POS data.
|
|
760
|
+
const treePosMap = new Map();
|
|
761
|
+
const treePredictionsMap = new Map();
|
|
762
|
+
Object.values(tree.pages).forEach((page) => {
|
|
763
|
+
page.grid.forEach((row) => {
|
|
764
|
+
row.forEach((btn) => {
|
|
765
|
+
if (!btn || !btn.label)
|
|
766
|
+
return;
|
|
767
|
+
const lower = btn.label.toLowerCase();
|
|
768
|
+
if (btn.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
|
|
769
|
+
treePosMap.set(lower, btn.pos);
|
|
770
|
+
}
|
|
771
|
+
if (btn.predictions && btn.predictions.length > 0) {
|
|
772
|
+
const existing = treePredictionsMap.get(lower);
|
|
773
|
+
if (!existing || btn.predictions.length > existing.length) {
|
|
774
|
+
treePredictionsMap.set(lower, btn.predictions);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
// For metrics buttons that lack POS but have a tree counterpart with POS,
|
|
781
|
+
// propagate the POS tag so it's available in the output.
|
|
782
|
+
buttons.forEach((btn) => {
|
|
783
|
+
const lower = btn.label.toLowerCase();
|
|
784
|
+
if (!btn.pos || btn.pos === 'Unknown' || btn.pos === 'Ignore') {
|
|
785
|
+
const treePos = treePosMap.get(lower);
|
|
786
|
+
if (treePos)
|
|
787
|
+
btn.pos = treePos;
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
// Note: buttons on pages unreachable via BFS from the root page are
|
|
791
|
+
// intentionally excluded. If there is no navigation path to a page,
|
|
792
|
+
// those buttons are not accessible to the user and should not count
|
|
793
|
+
// as available vocabulary.
|
|
596
794
|
// Iterate through all pages to find buttons with predictions
|
|
597
795
|
Object.values(tree.pages).forEach((page) => {
|
|
598
796
|
page.grid.forEach((row) => {
|
|
599
797
|
row.forEach((btn) => {
|
|
600
798
|
if (!btn || !btn.predictions || btn.predictions.length === 0)
|
|
601
799
|
return;
|
|
602
|
-
// Find the parent button's metrics
|
|
603
|
-
|
|
800
|
+
// Find the parent button's metrics (by id first, then by label)
|
|
801
|
+
let parentMetrics = buttons.find((b) => b.id === btn.id);
|
|
802
|
+
if (!parentMetrics && btn.label) {
|
|
803
|
+
parentMetrics = existingLabels.get(btn.label.toLowerCase());
|
|
804
|
+
}
|
|
604
805
|
if (!parentMetrics)
|
|
605
806
|
return;
|
|
606
807
|
// Calculate effort for each word form
|
|
@@ -698,7 +899,11 @@ export class MetricsCalculator {
|
|
|
698
899
|
// If no block assigned, treat as its own block at the end (fallback)
|
|
699
900
|
if (blockId === null) {
|
|
700
901
|
const loop = board.grid.length + (board.grid[0]?.length || 0);
|
|
701
|
-
return {
|
|
902
|
+
return {
|
|
903
|
+
steps: rowIndex + colIndex + 1,
|
|
904
|
+
selections: 1,
|
|
905
|
+
loopSteps: loop,
|
|
906
|
+
};
|
|
702
907
|
}
|
|
703
908
|
const blockConfig = board.scanBlocksConfig?.find((c) => c.id === blockId);
|
|
704
909
|
const blockOrder = blockConfig?.order ?? blockId;
|
|
@@ -131,8 +131,20 @@ export class VocabularyAnalyzer {
|
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
133
|
* Check if a word is in the board set
|
|
134
|
+
*
|
|
135
|
+
* Checks both direct button labels and smart grammar word forms
|
|
136
|
+
* (morphological inflections stored as predictions).
|
|
134
137
|
*/
|
|
135
138
|
hasWord(word, metrics) {
|
|
136
|
-
|
|
139
|
+
const lower = word.toLowerCase();
|
|
140
|
+
return metrics.buttons.some((b) => {
|
|
141
|
+
if (b.label.toLowerCase() === lower)
|
|
142
|
+
return true;
|
|
143
|
+
if (b.is_word_form && b.parent_button_label) {
|
|
144
|
+
if (b.label.toLowerCase() === lower)
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
137
149
|
}
|
|
138
150
|
}
|