@willwade/aac-processors 0.0.21 → 0.0.23
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/README.md +19 -27
- package/dist/core/treeStructure.d.ts +2 -2
- package/dist/core/treeStructure.js +4 -1
- package/dist/processors/applePanelsProcessor.js +166 -123
- package/dist/processors/astericsGridProcessor.js +121 -105
- package/dist/processors/dotProcessor.js +83 -65
- package/dist/processors/gridsetProcessor.js +2 -0
- package/dist/processors/obfProcessor.js +11 -4
- package/dist/processors/opmlProcessor.js +82 -44
- package/dist/processors/snapProcessor.js +19 -9
- package/dist/processors/touchchatProcessor.js +72 -21
- package/dist/utilities/analytics/metrics/core.d.ts +1 -1
- package/dist/utilities/analytics/metrics/core.js +191 -212
- package/dist/validation/applePanelsValidator.d.ts +10 -0
- package/dist/validation/applePanelsValidator.js +124 -0
- package/dist/validation/astericsValidator.d.ts +16 -0
- package/dist/validation/astericsValidator.js +115 -0
- package/dist/validation/dotValidator.d.ts +10 -0
- package/dist/validation/dotValidator.js +113 -0
- package/dist/validation/excelValidator.d.ts +10 -0
- package/dist/validation/excelValidator.js +89 -0
- package/dist/validation/index.d.ts +14 -1
- package/dist/validation/index.js +104 -1
- package/dist/validation/obfsetValidator.d.ts +10 -0
- package/dist/validation/obfsetValidator.js +103 -0
- package/dist/validation/opmlValidator.d.ts +10 -0
- package/dist/validation/opmlValidator.js +107 -0
- package/dist/validation/validationTypes.d.ts +22 -0
- package/dist/validation/validationTypes.js +38 -1
- package/dist/validation.d.ts +8 -2
- package/dist/validation.js +16 -1
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@ exports.getContrastingTextColor = getContrastingTextColor;
|
|
|
12
12
|
const baseProcessor_1 = require("../core/baseProcessor");
|
|
13
13
|
const treeStructure_1 = require("../core/treeStructure");
|
|
14
14
|
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const validation_1 = require("../validation");
|
|
15
17
|
const DEFAULT_COLOR_SCHEME_DEFINITIONS = [
|
|
16
18
|
{
|
|
17
19
|
name: 'CS_MODIFIED_FITZGERALD_KEY_VERY_LIGHT',
|
|
@@ -664,122 +666,136 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
664
666
|
}
|
|
665
667
|
loadIntoTree(filePathOrBuffer) {
|
|
666
668
|
const tree = new treeStructure_1.AACTree();
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
content =
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return tree;
|
|
677
|
-
}
|
|
678
|
-
const rawColorConfig = grdFile.metadata?.colorConfig;
|
|
679
|
-
const colorConfig = isRecord(rawColorConfig)
|
|
680
|
-
? rawColorConfig
|
|
681
|
-
: undefined;
|
|
682
|
-
const activeColorSchemeDefinition = getActiveColorSchemeDefinition(colorConfig);
|
|
683
|
-
// First pass: create all pages
|
|
684
|
-
grdFile.grids.forEach((grid) => {
|
|
685
|
-
const page = new treeStructure_1.AACPage({
|
|
686
|
-
id: grid.id,
|
|
687
|
-
name: this.getLocalizedLabel(grid.label) || grid.id,
|
|
688
|
-
grid: [],
|
|
689
|
-
buttons: [],
|
|
690
|
-
parentId: null,
|
|
691
|
-
style: {
|
|
692
|
-
backgroundColor: colorConfig?.gridBackgroundColor || '#FFFFFF',
|
|
693
|
-
borderColor: colorConfig?.elementBorderColor || '#CCCCCC',
|
|
694
|
-
borderWidth: colorConfig?.borderWidth || 1,
|
|
695
|
-
fontFamily: colorConfig?.fontFamily || 'Arial',
|
|
696
|
-
fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16, // Convert percentage to pixels, default to 16
|
|
697
|
-
fontColor: colorConfig?.fontColor || '#000000',
|
|
698
|
-
},
|
|
699
|
-
});
|
|
700
|
-
tree.addPage(page);
|
|
701
|
-
});
|
|
702
|
-
// Second pass: add buttons and establish navigation
|
|
703
|
-
grdFile.grids.forEach((grid) => {
|
|
704
|
-
const page = tree.getPage(grid.id);
|
|
705
|
-
if (!page)
|
|
706
|
-
return;
|
|
707
|
-
// Create a 2D grid to track button positions
|
|
708
|
-
const gridLayout = [];
|
|
709
|
-
const maxRows = Math.max(10, grid.rowCount || 10);
|
|
710
|
-
const maxCols = Math.max(10, grid.minColumnCount || 10);
|
|
711
|
-
for (let r = 0; r < maxRows; r++) {
|
|
712
|
-
gridLayout[r] = new Array(maxCols).fill(null);
|
|
669
|
+
const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.grd';
|
|
670
|
+
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
671
|
+
? filePathOrBuffer
|
|
672
|
+
: fs_1.default.readFileSync(filePathOrBuffer);
|
|
673
|
+
try {
|
|
674
|
+
let content = buffer.toString('utf-8');
|
|
675
|
+
// Remove BOM if present
|
|
676
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
677
|
+
content = content.slice(1);
|
|
713
678
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
679
|
+
const grdFile = JSON.parse(content);
|
|
680
|
+
if (!grdFile.grids) {
|
|
681
|
+
const validationResult = (0, validation_1.buildValidationResultFromMessage)({
|
|
682
|
+
filename,
|
|
683
|
+
filesize: buffer.byteLength,
|
|
684
|
+
format: 'asterics',
|
|
685
|
+
message: 'Missing grids array in Asterics .grd file',
|
|
686
|
+
type: 'structure',
|
|
687
|
+
description: 'Asterics grid collection',
|
|
688
|
+
});
|
|
689
|
+
throw new validation_1.ValidationFailureError('Invalid Asterics grid file', validationResult);
|
|
690
|
+
}
|
|
691
|
+
const rawColorConfig = grdFile.metadata?.colorConfig;
|
|
692
|
+
const colorConfig = isRecord(rawColorConfig)
|
|
693
|
+
? rawColorConfig
|
|
694
|
+
: undefined;
|
|
695
|
+
const activeColorSchemeDefinition = getActiveColorSchemeDefinition(colorConfig);
|
|
696
|
+
grdFile.grids.forEach((grid) => {
|
|
697
|
+
const page = new treeStructure_1.AACPage({
|
|
698
|
+
id: grid.id,
|
|
699
|
+
name: this.getLocalizedLabel(grid.label) || grid.id,
|
|
700
|
+
grid: [],
|
|
701
|
+
buttons: [],
|
|
702
|
+
parentId: null,
|
|
703
|
+
style: {
|
|
704
|
+
backgroundColor: colorConfig?.gridBackgroundColor || '#FFFFFF',
|
|
705
|
+
borderColor: colorConfig?.elementBorderColor || '#CCCCCC',
|
|
706
|
+
borderWidth: colorConfig?.borderWidth || 1,
|
|
707
|
+
fontFamily: colorConfig?.fontFamily || 'Arial',
|
|
708
|
+
fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16,
|
|
709
|
+
fontColor: colorConfig?.fontColor || '#000000',
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
tree.addPage(page);
|
|
713
|
+
});
|
|
714
|
+
grdFile.grids.forEach((grid) => {
|
|
715
|
+
const page = tree.getPage(grid.id);
|
|
716
|
+
if (!page)
|
|
717
|
+
return;
|
|
718
|
+
const gridLayout = [];
|
|
719
|
+
const maxRows = Math.max(10, grid.rowCount || 10);
|
|
720
|
+
const maxCols = Math.max(10, grid.minColumnCount || 10);
|
|
721
|
+
for (let r = 0; r < maxRows; r++) {
|
|
722
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
723
|
+
}
|
|
724
|
+
grid.gridElements.forEach((element) => {
|
|
725
|
+
const button = this.createButtonFromElement(element, colorConfig, activeColorSchemeDefinition);
|
|
726
|
+
page.addButton(button);
|
|
727
|
+
const buttonX = element.x || 0;
|
|
728
|
+
const buttonY = element.y || 0;
|
|
729
|
+
const buttonWidth = element.width || 1;
|
|
730
|
+
const buttonHeight = element.height || 1;
|
|
731
|
+
for (let r = buttonY; r < buttonY + buttonHeight && r < maxRows; r++) {
|
|
732
|
+
for (let c = buttonX; c < buttonX + buttonWidth && c < maxCols; c++) {
|
|
733
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
734
|
+
gridLayout[r][c] = button;
|
|
735
|
+
}
|
|
727
736
|
}
|
|
728
737
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
targetPage.parentId = page.id;
|
|
738
|
+
const navAction = element.actions.find((a) => a.modelName === 'GridActionNavigate');
|
|
739
|
+
const targetGridId = navAction && typeof navAction.toGridId === 'string' ? navAction.toGridId : undefined;
|
|
740
|
+
if (targetGridId) {
|
|
741
|
+
const targetPage = tree.getPage(targetGridId);
|
|
742
|
+
if (targetPage) {
|
|
743
|
+
targetPage.parentId = page.id;
|
|
744
|
+
}
|
|
737
745
|
}
|
|
738
|
-
}
|
|
746
|
+
});
|
|
747
|
+
page.grid = gridLayout;
|
|
739
748
|
});
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// Extract all unique languages from all grids and elements
|
|
751
|
-
const languages = new Set();
|
|
752
|
-
grdFile.grids.forEach((grid) => {
|
|
753
|
-
if (grid.label) {
|
|
754
|
-
Object.keys(grid.label).forEach((lang) => languages.add(lang));
|
|
755
|
-
}
|
|
756
|
-
grid.gridElements?.forEach((element) => {
|
|
757
|
-
if (element.label) {
|
|
758
|
-
Object.keys(element.label).forEach((lang) => languages.add(lang));
|
|
749
|
+
const astericsMetadata = {
|
|
750
|
+
format: 'asterics',
|
|
751
|
+
hasGlobalGrid: false,
|
|
752
|
+
};
|
|
753
|
+
if (grdFile.grids && grdFile.grids.length > 0) {
|
|
754
|
+
astericsMetadata.name = this.getLocalizedLabel(grdFile.grids[0].label);
|
|
755
|
+
const languages = new Set();
|
|
756
|
+
grdFile.grids.forEach((grid) => {
|
|
757
|
+
if (grid.label) {
|
|
758
|
+
Object.keys(grid.label).forEach((lang) => languages.add(lang));
|
|
759
759
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
760
|
+
grid.gridElements?.forEach((element) => {
|
|
761
|
+
if (element.label) {
|
|
762
|
+
Object.keys(element.label).forEach((lang) => languages.add(lang));
|
|
763
|
+
}
|
|
764
|
+
element.wordForms?.forEach((wf) => {
|
|
765
|
+
if (wf.lang)
|
|
766
|
+
languages.add(wf.lang);
|
|
767
|
+
});
|
|
764
768
|
});
|
|
765
769
|
});
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
770
|
+
if (languages.size > 0) {
|
|
771
|
+
astericsMetadata.languages = Array.from(languages).sort();
|
|
772
|
+
astericsMetadata.locale = languages.has('en')
|
|
773
|
+
? 'en'
|
|
774
|
+
: languages.has('de')
|
|
775
|
+
? 'de'
|
|
776
|
+
: astericsMetadata.languages[0];
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
tree.metadata = astericsMetadata;
|
|
780
|
+
if (grdFile.metadata && grdFile.metadata.homeGridId) {
|
|
781
|
+
tree.rootId = grdFile.metadata.homeGridId;
|
|
775
782
|
}
|
|
783
|
+
return tree;
|
|
776
784
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
785
|
+
catch (err) {
|
|
786
|
+
if (err instanceof validation_1.ValidationFailureError) {
|
|
787
|
+
throw err;
|
|
788
|
+
}
|
|
789
|
+
const validationResult = (0, validation_1.buildValidationResultFromMessage)({
|
|
790
|
+
filename,
|
|
791
|
+
filesize: buffer.byteLength,
|
|
792
|
+
format: 'asterics',
|
|
793
|
+
message: err?.message || 'Failed to parse Asterics grid file',
|
|
794
|
+
type: 'parse',
|
|
795
|
+
description: 'Parse Asterics grid JSON',
|
|
796
|
+
});
|
|
797
|
+
throw new validation_1.ValidationFailureError('Failed to load Asterics grid', validationResult, err);
|
|
781
798
|
}
|
|
782
|
-
return tree;
|
|
783
799
|
}
|
|
784
800
|
getLocalizedLabel(labelMap) {
|
|
785
801
|
if (!labelMap)
|
|
@@ -8,6 +8,8 @@ const baseProcessor_1 = require("../core/baseProcessor");
|
|
|
8
8
|
const treeStructure_1 = require("../core/treeStructure");
|
|
9
9
|
// Removed unused import: FileProcessor
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const validation_1 = require("../validation");
|
|
11
13
|
class DotProcessor extends baseProcessor_1.BaseProcessor {
|
|
12
14
|
constructor(options) {
|
|
13
15
|
super(options);
|
|
@@ -72,78 +74,94 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
72
74
|
return texts;
|
|
73
75
|
}
|
|
74
76
|
loadIntoTree(filePathOrBuffer) {
|
|
75
|
-
|
|
77
|
+
const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.dot';
|
|
78
|
+
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
79
|
+
? filePathOrBuffer
|
|
80
|
+
: fs_1.default.readFileSync(filePathOrBuffer);
|
|
81
|
+
const filesize = buffer.byteLength;
|
|
76
82
|
try {
|
|
77
|
-
content =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
const content = buffer.toString('utf8');
|
|
84
|
+
if (!content || content.trim().length === 0) {
|
|
85
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
86
|
+
filename,
|
|
87
|
+
filesize,
|
|
88
|
+
format: 'dot',
|
|
89
|
+
message: 'DOT file is empty',
|
|
90
|
+
type: 'content',
|
|
91
|
+
description: 'DOT file content',
|
|
92
|
+
});
|
|
93
|
+
throw new validation_1.ValidationFailureError('Empty DOT content', validation);
|
|
94
|
+
}
|
|
95
|
+
// Check for binary data (contains null bytes or non-printable characters)
|
|
96
|
+
const head = content.substring(0, 100);
|
|
97
|
+
for (let i = 0; i < head.length; i++) {
|
|
98
|
+
const code = head.charCodeAt(i);
|
|
99
|
+
if (code === 0 || (code >= 0 && code <= 8) || (code >= 14 && code <= 31)) {
|
|
100
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
101
|
+
filename,
|
|
102
|
+
filesize,
|
|
103
|
+
format: 'dot',
|
|
104
|
+
message: 'DOT appears to be binary data',
|
|
105
|
+
type: 'content',
|
|
106
|
+
description: 'DOT file content',
|
|
107
|
+
});
|
|
108
|
+
throw new validation_1.ValidationFailureError('Invalid DOT content', validation);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const { nodes, edges } = this.parseDotFile(content);
|
|
112
|
+
const tree = new treeStructure_1.AACTree();
|
|
113
|
+
tree.metadata.format = 'dot';
|
|
114
|
+
// Create pages for each node and add a self button representing the node label
|
|
115
|
+
for (const node of nodes) {
|
|
116
|
+
const page = new treeStructure_1.AACPage({
|
|
117
|
+
id: node.id,
|
|
118
|
+
name: node.label,
|
|
119
|
+
grid: [],
|
|
120
|
+
buttons: [],
|
|
121
|
+
parentId: null,
|
|
122
|
+
});
|
|
123
|
+
tree.addPage(page);
|
|
124
|
+
// Add a self button so single-node graphs yield one button
|
|
125
|
+
page.addButton(new treeStructure_1.AACButton({
|
|
126
|
+
id: `${node.id}_self`,
|
|
127
|
+
label: node.label,
|
|
128
|
+
message: node.label,
|
|
129
|
+
semanticAction: {
|
|
130
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
131
|
+
text: node.label,
|
|
132
|
+
fallback: { type: 'SPEAK', message: node.label },
|
|
133
|
+
},
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
// Create navigation buttons based on edges
|
|
137
|
+
for (const edge of edges) {
|
|
138
|
+
const fromPage = tree.getPage(edge.from);
|
|
139
|
+
if (fromPage) {
|
|
140
|
+
const button = new treeStructure_1.AACButton({
|
|
141
|
+
id: `nav_${edge.from}_${edge.to}`,
|
|
142
|
+
label: edge.label || edge.to,
|
|
143
|
+
message: '',
|
|
144
|
+
targetPageId: edge.to,
|
|
145
|
+
});
|
|
146
|
+
fromPage.addButton(button);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return tree;
|
|
81
150
|
}
|
|
82
151
|
catch (error) {
|
|
83
|
-
|
|
84
|
-
if (typeof filePathOrBuffer === 'string') {
|
|
152
|
+
if (error instanceof validation_1.ValidationFailureError) {
|
|
85
153
|
throw error;
|
|
86
154
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Check for binary data (contains null bytes or non-printable characters) without control-regex
|
|
95
|
-
const head = content.substring(0, 100);
|
|
96
|
-
let hasControl = false;
|
|
97
|
-
for (let i = 0; i < head.length; i++) {
|
|
98
|
-
const code = head.charCodeAt(i);
|
|
99
|
-
// Allow UTF-8 characters (code >= 127)
|
|
100
|
-
if (code === 0 || (code >= 0 && code <= 8) || (code >= 14 && code <= 31)) {
|
|
101
|
-
hasControl = true;
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (hasControl) {
|
|
106
|
-
return new treeStructure_1.AACTree();
|
|
107
|
-
}
|
|
108
|
-
const { nodes, edges } = this.parseDotFile(content);
|
|
109
|
-
const tree = new treeStructure_1.AACTree();
|
|
110
|
-
tree.metadata.format = 'dot';
|
|
111
|
-
// Create pages for each node and add a self button representing the node label
|
|
112
|
-
for (const node of nodes) {
|
|
113
|
-
const page = new treeStructure_1.AACPage({
|
|
114
|
-
id: node.id,
|
|
115
|
-
name: node.label,
|
|
116
|
-
grid: [],
|
|
117
|
-
buttons: [],
|
|
118
|
-
parentId: null,
|
|
155
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
156
|
+
filename,
|
|
157
|
+
filesize,
|
|
158
|
+
format: 'dot',
|
|
159
|
+
message: error?.message || 'Failed to parse DOT file',
|
|
160
|
+
type: 'parse',
|
|
161
|
+
description: 'Parse DOT graph',
|
|
119
162
|
});
|
|
120
|
-
|
|
121
|
-
// Add a self button so single-node graphs yield one button
|
|
122
|
-
page.addButton(new treeStructure_1.AACButton({
|
|
123
|
-
id: `${node.id}_self`,
|
|
124
|
-
label: node.label,
|
|
125
|
-
message: node.label,
|
|
126
|
-
semanticAction: {
|
|
127
|
-
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
128
|
-
text: node.label,
|
|
129
|
-
fallback: { type: 'SPEAK', message: node.label },
|
|
130
|
-
},
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
// Create navigation buttons based on edges
|
|
134
|
-
for (const edge of edges) {
|
|
135
|
-
const fromPage = tree.getPage(edge.from);
|
|
136
|
-
if (fromPage) {
|
|
137
|
-
const button = new treeStructure_1.AACButton({
|
|
138
|
-
id: `nav_${edge.from}_${edge.to}`,
|
|
139
|
-
label: edge.label || edge.to,
|
|
140
|
-
message: '',
|
|
141
|
-
targetPageId: edge.to,
|
|
142
|
-
});
|
|
143
|
-
fromPage.addButton(button);
|
|
144
|
-
}
|
|
163
|
+
throw new validation_1.ValidationFailureError('Failed to load DOT file', validation, error);
|
|
145
164
|
}
|
|
146
|
-
return tree;
|
|
147
165
|
}
|
|
148
166
|
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
149
167
|
const safeBuffer = Buffer.isBuffer(filePathOrBuffer)
|
|
@@ -1400,6 +1400,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1400
1400
|
const homeGridId = gridNameToIdMap.get(startGridName);
|
|
1401
1401
|
if (homeGridId) {
|
|
1402
1402
|
metadata.defaultHomePageId = homeGridId;
|
|
1403
|
+
// Also set tree.rootId so BoardViewer knows which page to show first
|
|
1404
|
+
tree.rootId = homeGridId;
|
|
1403
1405
|
}
|
|
1404
1406
|
}
|
|
1405
1407
|
const keyboardGridName = settingsData?.GridSetSettings?.KeyboardGrid ||
|
|
@@ -29,6 +29,12 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
29
29
|
}
|
|
30
30
|
processBoard(boardData, _boardPath) {
|
|
31
31
|
const sourceButtons = boardData.buttons || [];
|
|
32
|
+
// Calculate page ID first (used to make button IDs unique)
|
|
33
|
+
const pageId = _boardPath && _boardPath.endsWith('.obf') && !_boardPath.includes('/')
|
|
34
|
+
? _boardPath // Zip entry - use filename to match navigation paths
|
|
35
|
+
: boardData?.id
|
|
36
|
+
? String(boardData.id)
|
|
37
|
+
: _boardPath?.split('/').pop() || '';
|
|
32
38
|
const buttons = sourceButtons.map((btn) => {
|
|
33
39
|
const semanticAction = btn.load_board
|
|
34
40
|
? {
|
|
@@ -50,7 +56,8 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
50
56
|
},
|
|
51
57
|
};
|
|
52
58
|
return new treeStructure_1.AACButton({
|
|
53
|
-
|
|
59
|
+
// Make button ID unique by combining page ID and button ID
|
|
60
|
+
id: `${pageId}::${btn?.id || ''}`,
|
|
54
61
|
label: String(btn?.label || ''),
|
|
55
62
|
message: String(btn?.vocalization || btn?.label || ''),
|
|
56
63
|
visibility: mapObfVisibility(btn.hidden),
|
|
@@ -65,7 +72,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
65
72
|
});
|
|
66
73
|
const buttonMap = new Map(buttons.map((btn) => [btn.id, btn]));
|
|
67
74
|
const page = new treeStructure_1.AACPage({
|
|
68
|
-
id:
|
|
75
|
+
id: pageId, // Use the page ID we calculated earlier
|
|
69
76
|
name: String(boardData?.name || ''),
|
|
70
77
|
grid: [],
|
|
71
78
|
buttons,
|
|
@@ -96,7 +103,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
96
103
|
return;
|
|
97
104
|
if (rowIndex >= rows || colIndex >= cols)
|
|
98
105
|
return;
|
|
99
|
-
const aacBtn = buttonMap.get(
|
|
106
|
+
const aacBtn = buttonMap.get(`${pageId}::${cellId}`);
|
|
100
107
|
if (aacBtn) {
|
|
101
108
|
grid[rowIndex][colIndex] = aacBtn;
|
|
102
109
|
}
|
|
@@ -109,7 +116,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
109
116
|
const row = Math.floor(btn.box_id / cols);
|
|
110
117
|
const col = btn.box_id % cols;
|
|
111
118
|
if (row < rows && col < cols) {
|
|
112
|
-
const aacBtn = buttonMap.get(
|
|
119
|
+
const aacBtn = buttonMap.get(`${pageId}::${btn.id}`);
|
|
113
120
|
if (aacBtn) {
|
|
114
121
|
grid[row][col] = aacBtn;
|
|
115
122
|
}
|
|
@@ -9,6 +9,8 @@ const treeStructure_1 = require("../core/treeStructure");
|
|
|
9
9
|
// Removed unused import: FileProcessor
|
|
10
10
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const validation_1 = require("../validation");
|
|
12
14
|
class OpmlProcessor extends baseProcessor_1.BaseProcessor {
|
|
13
15
|
constructor(options) {
|
|
14
16
|
super(options);
|
|
@@ -88,55 +90,91 @@ class OpmlProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
88
90
|
return texts;
|
|
89
91
|
}
|
|
90
92
|
loadIntoTree(filePathOrBuffer) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
// Validate XML before parsing, fast-xml-parser is permissive by default
|
|
98
|
-
const validationResult = fast_xml_parser_1.XMLValidator.validate(content);
|
|
99
|
-
if (validationResult !== true) {
|
|
100
|
-
const reason = validationResult?.err?.msg || JSON.stringify(validationResult);
|
|
101
|
-
throw new Error(`Invalid OPML XML: ${reason}`);
|
|
102
|
-
}
|
|
103
|
-
const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
|
|
104
|
-
let data;
|
|
93
|
+
const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.opml';
|
|
94
|
+
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
95
|
+
? filePathOrBuffer
|
|
96
|
+
: fs_1.default.readFileSync(filePathOrBuffer);
|
|
97
|
+
const content = buffer.toString('utf8');
|
|
105
98
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return tree; // Return empty tree if no outline data
|
|
117
|
-
}
|
|
118
|
-
const outlines = Array.isArray(bodyOutline) ? bodyOutline : [bodyOutline];
|
|
119
|
-
let firstRootId = null;
|
|
120
|
-
outlines.forEach((outline) => {
|
|
121
|
-
const { page, childPages } = this.processOutline(outline);
|
|
122
|
-
if (page && page.id) {
|
|
123
|
-
tree.addPage(page);
|
|
124
|
-
if (!firstRootId)
|
|
125
|
-
firstRootId = page.id;
|
|
99
|
+
if (!content || !content.trim()) {
|
|
100
|
+
const validationResult = (0, validation_1.buildValidationResultFromMessage)({
|
|
101
|
+
filename,
|
|
102
|
+
filesize: buffer.byteLength,
|
|
103
|
+
format: 'opml',
|
|
104
|
+
message: 'Empty OPML content',
|
|
105
|
+
type: 'content',
|
|
106
|
+
description: 'OPML content is empty',
|
|
107
|
+
});
|
|
108
|
+
throw new validation_1.ValidationFailureError('Empty OPML content', validationResult);
|
|
126
109
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
// Validate XML before parsing, fast-xml-parser is permissive by default
|
|
111
|
+
const validationResult = fast_xml_parser_1.XMLValidator.validate(content);
|
|
112
|
+
if (validationResult !== true) {
|
|
113
|
+
const reason = validationResult?.err?.msg || JSON.stringify(validationResult);
|
|
114
|
+
const structured = (0, validation_1.buildValidationResultFromMessage)({
|
|
115
|
+
filename,
|
|
116
|
+
filesize: buffer.byteLength,
|
|
117
|
+
format: 'opml',
|
|
118
|
+
message: `Invalid OPML XML: ${reason}`,
|
|
119
|
+
type: 'xml',
|
|
120
|
+
description: 'OPML XML validation',
|
|
121
|
+
});
|
|
122
|
+
throw new validation_1.ValidationFailureError('Invalid OPML XML', structured);
|
|
123
|
+
}
|
|
124
|
+
const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
|
|
125
|
+
const data = parser.parse(content);
|
|
126
|
+
const tree = new treeStructure_1.AACTree();
|
|
127
|
+
tree.metadata.format = 'opml';
|
|
128
|
+
// Handle case where body.outline might not exist or be in different formats
|
|
129
|
+
const bodyOutline = data.opml?.body?.outline;
|
|
130
|
+
if (!bodyOutline) {
|
|
131
|
+
const structured = (0, validation_1.buildValidationResultFromMessage)({
|
|
132
|
+
filename,
|
|
133
|
+
filesize: buffer.byteLength,
|
|
134
|
+
format: 'opml',
|
|
135
|
+
message: 'Missing body.outline in OPML document',
|
|
136
|
+
type: 'structure',
|
|
137
|
+
description: 'OPML outline root',
|
|
138
|
+
});
|
|
139
|
+
throw new validation_1.ValidationFailureError('Invalid OPML structure', structured);
|
|
140
|
+
}
|
|
141
|
+
const outlines = Array.isArray(bodyOutline) ? bodyOutline : [bodyOutline];
|
|
142
|
+
let firstRootId = null;
|
|
143
|
+
outlines.forEach((outline) => {
|
|
144
|
+
const { page, childPages } = this.processOutline(outline);
|
|
145
|
+
if (page && page.id) {
|
|
146
|
+
tree.addPage(page);
|
|
147
|
+
if (!firstRootId)
|
|
148
|
+
firstRootId = page.id;
|
|
149
|
+
}
|
|
150
|
+
childPages.forEach((childPage) => {
|
|
151
|
+
if (childPage && childPage.id)
|
|
152
|
+
tree.addPage(childPage);
|
|
153
|
+
});
|
|
130
154
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
// Set rootId to first root page, or fallback to first page if any exist
|
|
156
|
+
if (firstRootId) {
|
|
157
|
+
tree.rootId = firstRootId;
|
|
158
|
+
}
|
|
159
|
+
else if (Object.keys(tree.pages).length > 0) {
|
|
160
|
+
tree.rootId = Object.keys(tree.pages)[0];
|
|
161
|
+
}
|
|
162
|
+
return tree;
|
|
135
163
|
}
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
catch (err) {
|
|
165
|
+
if (err instanceof validation_1.ValidationFailureError) {
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
const validationResult = (0, validation_1.buildValidationResultFromMessage)({
|
|
169
|
+
filename,
|
|
170
|
+
filesize: buffer.byteLength,
|
|
171
|
+
format: 'opml',
|
|
172
|
+
message: err?.message || 'Failed to parse OPML',
|
|
173
|
+
type: 'parse',
|
|
174
|
+
description: 'Parse OPML XML',
|
|
175
|
+
});
|
|
176
|
+
throw new validation_1.ValidationFailureError('Failed to load OPML file', validationResult, err);
|
|
138
177
|
}
|
|
139
|
-
return tree;
|
|
140
178
|
}
|
|
141
179
|
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
142
180
|
const content = typeof filePathOrBuffer === 'string'
|