(raw: string)
| 127 | * Returns `null` for an empty folder (the token is left literal by the caller). |
| 128 | */ |
| 129 | export function parseFileToken(raw: string): ParsedFileToken | null { |
| 130 | if (!raw) return null; |
| 131 | |
| 132 | const allParts = splitPipeParts(raw); |
| 133 | const folderPath = (allParts.shift() ?? "").trim(); |
| 134 | if (!folderPath) return null; |
| 135 | |
| 136 | // Peel bare flags first. |
| 137 | let remaining = allParts; |
| 138 | const optionalResult = extractBareFlagPart(remaining, "optional"); |
| 139 | remaining = optionalResult.remaining; |
| 140 | const optional = optionalResult.found; |
| 141 | |
| 142 | const customResult = extractBareFlagPart(remaining, "custom"); |
| 143 | remaining = customResult.remaining; |
| 144 | const allowCustomInput = customResult.found; |
| 145 | |
| 146 | const multiResult = extractBareFlagPart(remaining, "multi"); |
| 147 | remaining = multiResult.remaining; |
| 148 | const bareMultiSelect = multiResult.found; |
| 149 | |
| 150 | let mode: FileMode = "name"; |
| 151 | const afterMode: string[] = []; |
| 152 | for (const part of remaining) { |
| 153 | const trimmed = part.trim().toLowerCase(); |
| 154 | if (MODE_FLAGS.has(trimmed as FileMode)) { |
| 155 | mode = trimmed as FileMode; // last one wins |
| 156 | continue; |
| 157 | } |
| 158 | afterMode.push(part); |
| 159 | } |
| 160 | |
| 161 | // Peel key:value options that are FILE-specific (label/name); everything else |
| 162 | // (tag/exclude-*) is parsed by FieldSuggestionParser below. |
| 163 | let label: string | undefined; |
| 164 | let aliasName: string | undefined; |
| 165 | for (const part of afterMode) { |
| 166 | const parsed = parsePipeKeyValue(part); |
| 167 | if (!parsed) continue; |
| 168 | if (parsed.key === "label" && parsed.value) label = parsed.value; |
| 169 | else if (parsed.key === "name" && parsed.value) aliasName = parsed.value; |
| 170 | } |
| 171 | |
| 172 | // Delegate filter parsing to the shared FIELD parser (it skips unknown keys |
| 173 | // like label/name and bare flags), then force the scope folder. The first |
| 174 | // segment is the folder, so any `|folder:` sub-option the parser picks up is |
| 175 | // intentionally discarded below (the positional folder always wins). |
| 176 | const fieldParsed = FieldSuggestionParser.parse(raw); |
| 177 | const multiSelect = bareMultiSelect || (fieldParsed.multiSelect ?? false); |
| 178 | const filter: FieldFilter = { |
| 179 | folder: folderPath, |
| 180 | tags: fieldParsed.filters.tags, |
| 181 | excludeFolders: fieldParsed.filters.excludeFolders, |
| 182 | excludeTags: fieldParsed.filters.excludeTags, |
| 183 | excludeFiles: fieldParsed.filters.excludeFiles, |
| 184 | }; |
| 185 | |
| 186 | // Sharing (`|name:`) is scoped to the picker's SCOPE (folder + filters): a pick |
no test coverage detected