forked from actions/typescript-action
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxpath-utils.ts
More file actions
126 lines (114 loc) · 3.96 KB
/
xpath-utils.ts
File metadata and controls
126 lines (114 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import xpath, { type XString, type XBoolean, type Evaluator } from 'xpath';
// Extend xpath types as they are incomplete.
declare module 'xpath' {
/** A string. */
export class XString {
toString(): string;
}
/** A boolean. */
export class XBoolean {
toString(): string;
booleanValue(): boolean;
}
/** A set of nodes. */
export class XNodeSet {
nodes: Node[];
size: number;
toString(): string;
}
interface EvaluatorOptions {
node: Node;
functions: Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(context: unknown, ...args: any[]) => unknown
>;
}
/** Evaluate the expression and return the result in the requested type. */
export interface Evaluator {
evaluateString: (options: EvaluatorOptions) => string;
evaluateNumber: (options: EvaluatorOptions) => number;
evaluateBoolean: (options: EvaluatorOptions) => boolean;
}
/** Parse the expression and return an evaluator. */
export function parse(expr: string): Evaluator;
}
/** Additional xpath functions not built-in the xpath module. */
const functions = {
/** Replace all occurrences of a string with another string. */
replace(
_context: unknown,
input: XString,
search: XString,
replace: XString,
): string {
return `${input}`.replace(new RegExp(`${search}`), `${replace}`) ?? '';
},
/** Match a string against a regular expression and return the first group or empty string. */
match(_context: unknown, input: XString, pattern: XString): string {
return `${input}`.match(new RegExp(`${pattern}`))?.[1] ?? '';
},
/** Return based on boolean condition */
if(
_context: unknown,
condition: XBoolean,
then: Node,
otherwise: Node,
): Node {
return condition.booleanValue() ? then : otherwise;
},
/** Trim & collapse whitespace from the input string, except newlines. */
normalize(_context: unknown, input: XString): string {
return `${input}`.replaceAll(/^\s+|\s+$/gm, '');
},
};
/** Utility to select items from a Node with extra functions like `replace`. */
export const xpathSelect = (node: Node) => ({
/** Parse the expression and return an evaluator. */
parse(expression: string): Evaluator {
try {
return xpath.parse(expression);
} catch (error) {
// Xpath does not show the expression in the error message.
const msg = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Error parsing xpath expression "${expression}": ${msg}`);
}
},
/** Evaluate the expression and return the result as a string. */
string(expression: string): string {
return this.parse(expression).evaluateString({ node, functions });
},
/** Evaluate the expression and return the result as a number. */
number(expression: string): number {
return this.parse(expression).evaluateNumber({ node, functions });
},
/** Evaluate the expression and return the result as a boolean. */
boolean(expression: string): boolean {
return this.parse(expression).evaluateBoolean({ node, functions });
},
});
// Re-export the xpath module with the extended types and functions.
export * from 'xpath';
/** Lightweight type guard for a DOM-like Node. */
export function isNodeLike(value: unknown): value is Node {
return (
typeof value === 'object' &&
value !== null &&
// Some DOM implementations have nodeType & nodeName
'nodeType' in (value as Record<string, unknown>) &&
'nodeName' in (value as Record<string, unknown>)
);
}
/** Returns true if the provided value is an array of Node objects. */
export function isArrayOfNodes(value: unknown): value is Node[] {
return Array.isArray(value) && value.every(isNodeLike);
}
/** Passthrough to original xpath select while keeping ESM re-export pattern. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const select: any =
(xpath as unknown as { select?: unknown }).select ||
// Fallback for unexpected shape; users should provide proper xpath usage.
/* istanbul ignore next */
(() => {
throw new Error('xpath.select is not available in the imported module');
});