• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FontoXML / fontoxpath / 4162531615

pending completion
4162531615

Pull #577

github

GitHub
Merge 1ac7216f1 into a951decaa
Pull Request #577: Dramatically improve parse errors

4894 of 5691 branches covered (86.0%)

Branch coverage included in aggregate %.

30 of 31 new or added lines in 7 files covered. (96.77%)

2 existing lines in 2 files now uncovered.

10527 of 11166 relevant lines covered (94.28%)

97410.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.89
/src/parseScript.ts
1
import domBackedDocumentWriter from './documentWriter/domBackedDocumentWriter';
9✔
2
import IDocumentWriter from './documentWriter/IDocumentWriter';
3
import ExternalDomFacade from './domFacade/ExternalDomFacade';
9✔
4
import { printAndRethrowError } from './evaluationUtils/printAndRethrowError';
9✔
5
import { sequenceTypeToString } from './expressions/dataTypes/Value';
9✔
6
import ExecutionSpecificStaticContext from './expressions/ExecutionSpecificStaticContext';
9✔
7
import { BUILT_IN_NAMESPACE_URIS } from './expressions/staticallyKnownNamespaces';
8
import StaticContext from './expressions/StaticContext';
9✔
9
import ISimpleNodesFactory from './nodesFactory/ISimpleNodesFactory';
10
import astHelper, { IAST } from './parsing/astHelper';
9✔
11
import normalizeEndOfLines from './parsing/normalizeEndOfLines';
9✔
12
import parseExpression from './parsing/parseExpression';
9✔
13
import processProlog from './parsing/processProlog';
9✔
14
import annotateAst from './typeInference/annotateAST';
9✔
15
import { AnnotationContext } from './typeInference/AnnotationContext';
9✔
16
import { Language, Options } from './types/Options';
9✔
17
import { Element, Text } from './types/Types';
18

19
const PREFERRED_PREFIX_BY_NAMESPACEURI: { [prefix: string]: string } = {
9✔
20
        [BUILT_IN_NAMESPACE_URIS.XQUERYX_NAMESPACE_URI]: 'xqx',
21
        [BUILT_IN_NAMESPACE_URIS.XQUERYX_UPDATING_NAMESPACE_URI]: 'xquf',
22
        [BUILT_IN_NAMESPACE_URIS.FONTOXPATH_NAMESPACE_URI]: 'x',
23
};
24

25
function getQName(name: string, parentUri: string): { localName: string; namespaceUri: string } {
26
        switch (name) {
265,314✔
27
                case 'copySource':
28
                case 'insertAfter':
29
                case 'insertAsFirst':
30
                case 'insertAsLast':
31
                case 'insertBefore':
32
                case 'insertInto':
33
                case 'modifyExpr':
34
                case 'newNameExpr':
35
                case 'replacementExpr':
36
                case 'replaceValue':
37
                case 'returnExpr':
38
                case 'sourceExpr':
39
                case 'targetExpr':
40
                case 'transformCopies':
41
                case 'transformCopy':
42
                        // Some 'vanilla' elements are actually XQueryX elements. Check parent
43
                        return {
13,548✔
44
                                localName: name,
45
                                namespaceUri: parentUri || BUILT_IN_NAMESPACE_URIS.XQUERYX_NAMESPACE_URI,
13,548!
46
                        };
47
                case 'deleteExpr':
48
                case 'insertExpr':
49
                case 'renameExpr':
50
                case 'replaceExpr':
51
                case 'transformExpr':
52
                        // Elements added in the update facility need to be in a different namespace
53
                        return {
5,145✔
54
                                localName: name,
55
                                namespaceUri: BUILT_IN_NAMESPACE_URIS.XQUERYX_UPDATING_NAMESPACE_URI,
56
                        };
57
                case 'x:stackTrace':
58
                        // Custom AST nodes introduced by fontoxpath for debugging
59
                        return {
36✔
60
                                localName: 'stackTrace',
61
                                namespaceUri: BUILT_IN_NAMESPACE_URIS.FONTOXPATH_NAMESPACE_URI,
62
                        };
63
                default:
64
                        return {
246,585✔
65
                                localName: name,
66
                                namespaceUri: BUILT_IN_NAMESPACE_URIS.XQUERYX_NAMESPACE_URI,
67
                        };
68
        }
69
}
70

71
/**
72
 * Transform the given JsonML fragment into the corresponding DOM structure, using the given document to
73
 * create nodes.
74
 *
75
 * JsonML is always expected to be a JavaScript structure. If you have a string of JSON, use JSON.parse first.
76
 *
77
 * @param   documentWriter -  Used to place nodes in the DOM
78
 * @param   simpleNodesFactory   -  Used to construct nodes
79
 * @param   jsonml   -  The JsonML fragment to parse
80
 * @param   parentUri
81
 * @param   skipEmptyPrefixes - Whether to output empty prefixes. This is closed to the spec XQueryX examples, but breaks assumptions in AST annotation
82
 *
83
 * @return  The root node of the constructed DOM fragment
84
 */
85
function parseNode(
86
        documentWriter: IDocumentWriter,
87
        simpleNodesFactory: ISimpleNodesFactory,
88
        jsonml: any[] | string,
89
        parentUri: string | null,
90
        skipEmptyPrefixes: boolean
91
): Text | Element {
92
        if (typeof jsonml === 'string') {
343,872✔
93
                if (jsonml.length === 0) {
78,558✔
94
                        return null;
54✔
95
                }
96
                return simpleNodesFactory.createTextNode(jsonml);
78,504✔
97
        }
98

99
        if (!Array.isArray(jsonml)) {
265,314!
100
                throw new TypeError('JsonML element should be an array or string');
×
101
        }
102

103
        const qName = getQName(jsonml[0], parentUri);
265,314✔
104
        const name = qName.localName;
265,314✔
105
        const namespaceUri = qName.namespaceUri;
265,314✔
106

107
        // Node must be a normal element
108
        const element = simpleNodesFactory.createElementNS(
265,314✔
109
                namespaceUri,
110
                PREFERRED_PREFIX_BY_NAMESPACEURI[namespaceUri] + ':' + name
111
        );
112
        const firstChild = jsonml[1];
265,314✔
113
        let firstChildIndex = 1;
265,314✔
114
        if (typeof firstChild === 'object' && !Array.isArray(firstChild)) {
265,314✔
115
                if (firstChild !== null) {
75,261!
116
                        for (const attributeName of Object.keys(firstChild)) {
75,261✔
117
                                let attributeValue = firstChild[attributeName];
120,957✔
118
                                if (attributeValue === null) {
120,957✔
119
                                        continue;
48,384✔
120
                                }
121
                                if (attributeName === 'type') {
72,573✔
122
                                        // TODO: prevent writing undefined to variables at the first place
123
                                        if (attributeValue !== undefined) {
27,540✔
124
                                                documentWriter.setAttributeNS(
25,710✔
125
                                                        element,
126
                                                        namespaceUri,
127
                                                        'fontoxpath:' + attributeName,
128
                                                        sequenceTypeToString(attributeValue)
129
                                                );
130
                                        }
131
                                        continue;
27,540✔
132
                                }
133
                                if (
45,033✔
134
                                        (attributeName === 'start' || attributeName === 'end') &&
90,102✔
135
                                        name === 'stackTrace'
136
                                ) {
137
                                        attributeValue = JSON.stringify(attributeValue);
72✔
138
                                }
139
                                if (skipEmptyPrefixes && attributeName === 'prefix' && attributeValue === '') {
45,033✔
140
                                        continue;
22,305✔
141
                                }
142
                                documentWriter.setAttributeNS(
22,728✔
143
                                        element,
144
                                        namespaceUri,
145
                                        PREFERRED_PREFIX_BY_NAMESPACEURI[namespaceUri] + ':' + attributeName,
146
                                        attributeValue
147
                                );
148
                        }
149
                }
150

151
                firstChildIndex = 2;
75,261✔
152
        }
153
        // Parse children
154
        for (let i = firstChildIndex, l = jsonml.length; i < l; ++i) {
265,314✔
155
                const node = parseNode(
338,220✔
156
                        documentWriter,
157
                        simpleNodesFactory,
158
                        jsonml[i] as any[] | string,
159
                        namespaceUri,
160
                        skipEmptyPrefixes
161
                );
162
                if (node !== null) {
338,220✔
163
                        documentWriter.insertBefore(element, node, null);
338,166✔
164
                }
165
        }
166

167
        return element;
265,314✔
168
}
169

170
/**
171
 * Parse an XPath or XQuery script and output it as an XQueryX element. Refer to the [XQueryX
172
 * spec](https://www.w3.org/TR/xqueryx-31/) for more info.
173
 *
174
 * The precise generated XQueryX may change in the future when progress is made on supporting the
175
 * XQueryX test set provided with the [QT3 test suite](https://dev.w3.org/2011/QT3-test-suite/).
176
 *
177
 * Note that the parseScript function returns a detached element: it is not added to the passed
178
 * document.
179
 *
180
 * The element also contains the original expression as a comment.
181
 *
182
 * This may later be used for error processing to display the full original script instead of only referring to the AST.
183
 *
184
 * @example
185
 * Parse "self::element" to an XQueryX element and access it
186
 * ```
187
 * const xqueryx = parseScript(
188
 *   'self::element',
189
 *   {
190
 *     language: evaluateXPath.XPATH_3_1_LANGUAGE
191
 *   },
192
 *   new slimdom.Document()
193
 * );
194
 *
195
 * // Get the nametest element
196
 * const nameTestElement = evaluateXPathToFirstNode(
197
 *   'descendant-or-self::Q{http://www.w3.org/2005/XQueryX}nameTest',
198
 *   xqueryx)
199
 * ```
200
 *
201
 * @public
202
 *
203
 * @param script - The script to parse
204
 *
205
 * @param options - Additional options for parsing. Can be used to switch between parsing XPath or
206
 * XQuery update facility
207
 *
208
 * @param simpleNodesFactory - A NodesFactory will be used to create the DOM. This can be a
209
 * reference to the document in which the XQueryX will be created
210
 *
211
 * @param documentWriter - The documentWriter will be used to append children to the newly created
212
 * dom
213
 */
214
export default function parseScript<TElement extends Element>(
9✔
215
        script: string,
216
        options: Options & { annotateAst?: boolean },
217
        simpleNodesFactory: ISimpleNodesFactory,
218
        documentWriter: IDocumentWriter = domBackedDocumentWriter
5,652✔
219
): TElement {
220
        script = normalizeEndOfLines(script);
5,652✔
221
        let ast: IAST;
222
        try {
5,652✔
223
                ast = parseExpression(script, {
5,652✔
224
                        allowXQuery:
225
                                options['language'] === Language.XQUERY_3_1_LANGUAGE ||
8,730✔
226
                                options['language'] === Language.XQUERY_UPDATE_3_1_LANGUAGE,
227
                        debug: options.debug,
228
                });
229
        } catch (error) {
NEW
230
                printAndRethrowError(script, error);
×
231
        }
232

233
        // Let external options required for static evaluation flow in
234
        const executionSpecificStaticContext = new ExecutionSpecificStaticContext(
5,652✔
235
                options['namespaceResolver'] || ((_prefix: string) => null),
16,884✔
236
                {},
237
                options['defaultFunctionNamespaceURI'] === undefined
5,652!
238
                        ? BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI
239
                        : options['defaultFunctionNamespaceURI'],
240
                options['functionNameResolver'] || (() => null)
966✔
241
        );
242
        const rootStaticContext = new StaticContext(executionSpecificStaticContext);
5,652✔
243
        const anyModuleDecl = astHelper.getFirstChild(ast, ['mainModule', 'libraryModule']);
5,652✔
244
        // Spike the static context if we are actually a library module
245
        const moduleDecl = astHelper.getFirstChild(anyModuleDecl, 'moduleDecl');
5,652✔
246
        if (moduleDecl) {
5,652✔
247
                const prefix = astHelper.getTextContent(astHelper.getFirstChild(moduleDecl, 'prefix'));
3✔
248
                const uri = astHelper.getTextContent(astHelper.getFirstChild(moduleDecl, 'uri'));
3✔
249

250
                rootStaticContext.registerNamespace(prefix, uri);
3✔
251
        }
252

253
        const prolog = astHelper.getFirstChild(anyModuleDecl, 'prolog');
5,652✔
254

255
        if (prolog) {
5,652✔
256
                processProlog(prolog, rootStaticContext, false, script);
5,127✔
257
        }
258

259
        if (options['annotateAst'] !== false) {
5,652✔
260
                const context = new AnnotationContext(rootStaticContext);
2,754✔
261
                annotateAst(ast, context);
2,754✔
262
        }
263

264
        const domFacade = new ExternalDomFacade();
5,652✔
265
        const astAsXML = parseNode(
5,652✔
266
                documentWriter,
267
                simpleNodesFactory,
268
                ast,
269
                null,
270
                options.annotateAst === false
271
        ) as TElement;
272
        documentWriter.insertBefore(
5,652✔
273
                astAsXML,
274
                simpleNodesFactory.createComment(script),
275
                domFacade['getFirstChild'](astAsXML)
276
        );
277

278
        return astAsXML;
5,652✔
279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc