-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathworkflow-references.ts
More file actions
95 lines (79 loc) · 3.5 KB
/
Copy pathworkflow-references.ts
File metadata and controls
95 lines (79 loc) · 3.5 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
const REFERENCE_START = '<'
const REFERENCE_END = '>'
const REFERENCE_PATH_DELIMITER = '.'
const INVALID_REFERENCE_CHARS = /[+*/=<>!&|]/
const LEADING_REFERENCE_PATTERN = /^[<>=!\s]*$/
const ENV_REFERENCE_PATTERN = /\{\{[^{}\r\n]+\}\}/g
export type WorkflowReferenceTokenKind = 'environment' | 'workflow'
export interface WorkflowReferenceToken {
kind: WorkflowReferenceTokenKind
value: string
start: number
end: number
}
/** Separates comparison characters before the final `<workflow.reference>` segment. */
export function splitWorkflowReferenceSegment(
segment: string
): { leading: string; reference: string } | null {
if (!segment.startsWith(REFERENCE_START) || !segment.endsWith(REFERENCE_END)) return null
const lastOpenBracket = segment.lastIndexOf(REFERENCE_START)
if (lastOpenBracket === -1) return null
const leading = lastOpenBracket > 0 ? segment.slice(0, lastOpenBracket) : ''
const reference = segment.slice(lastOpenBracket)
if (!reference.startsWith(REFERENCE_START) || !reference.endsWith(REFERENCE_END)) return null
return { leading, reference }
}
/** Distinguishes Sim workflow references from comparison expressions and stray angle brackets. */
export function isLikelyWorkflowReferenceSegment(segment: string): boolean {
const split = splitWorkflowReferenceSegment(segment)
if (!split) return false
const { leading, reference } = split
if (leading && !LEADING_REFERENCE_PATTERN.test(leading)) return false
const inner = reference.slice(REFERENCE_START.length, -REFERENCE_END.length)
if (!inner || inner.startsWith(' ')) return false
if (/^\s*[<>=!]+\s*$/.test(inner) || /\s[<>=!]+\s/.test(inner)) return false
if (/^[<>=!]+\s/.test(inner)) return false
const dotIndex = inner.indexOf(REFERENCE_PATH_DELIMITER)
if (dotIndex !== -1) {
const beforeDot = inner.slice(0, dotIndex)
const afterDot = inner.slice(dotIndex + REFERENCE_PATH_DELIMITER.length)
return (
!afterDot.includes(' ') &&
!INVALID_REFERENCE_CHARS.test(beforeDot) &&
!INVALID_REFERENCE_CHARS.test(afterDot)
)
}
return !INVALID_REFERENCE_CHARS.test(inner) && !/^\d+$/.test(inner) && !/\s\d/.test(inner)
}
/** Finds non-overlapping `{{ENV}}` and `<workflow.reference>` tokens in source order. */
export function findWorkflowReferenceTokens(source: string): WorkflowReferenceToken[] {
const tokens: WorkflowReferenceToken[] = []
for (const match of source.matchAll(ENV_REFERENCE_PATTERN)) {
const start = match.index
tokens.push({ kind: 'environment', value: match[0], start, end: start + match[0].length })
}
let candidateStart = -1
for (let index = 0; index < source.length; index += 1) {
const character = source[index]
if (character === REFERENCE_START && candidateStart === -1) {
candidateStart = index
continue
}
if (character === '\r' || character === '\n') {
candidateStart = -1
continue
}
if (character !== REFERENCE_END || candidateStart === -1) continue
const candidate = source.slice(candidateStart, index + REFERENCE_END.length)
const split = splitWorkflowReferenceSegment(candidate)
if (split && isLikelyWorkflowReferenceSegment(candidate)) {
const start = candidateStart + split.leading.length
const end = start + split.reference.length
if (!tokens.some((token) => start < token.end && end > token.start)) {
tokens.push({ kind: 'workflow', value: split.reference, start, end })
}
}
candidateStart = -1
}
return tokens.sort((left, right) => left.start - right.start)
}