Line data Source code
1 : /// The character that starts a script reference in natural language, e.g. ":revenue".
2 : /// It follows the named parameters of Hyper, ":name".
3 : export const SCRIPT_REFERENCE_PREFIX = ':';
4 :
5 : /// A character that continues a script name, a reference must not be followed by one
6 1 : const NAME_CHAR = /[\p{L}\p{N}_$-]/u;
7 :
8 : /// A reference to a session script in natural language text
9 : export interface ScriptReference {
10 : /// The begin of the reference, at the prefix
11 : from: number;
12 : /// The end of the reference
13 : to: number;
14 : /// The display name of the referenced script
15 : name: string;
16 : }
17 :
18 : /// Render the reference of a script
19 1 : export function renderScriptReference(name: string): string {
20 1 : return `${SCRIPT_REFERENCE_PREFIX}${name}`;
21 1 : }
22 :
23 : /// Find the references to session scripts in a text.
24 : /// A reference is the prefix followed by the display name of a script, the longest name wins.
25 : /// Names compare case-insensitively, the prefix must not be part of a word ("12:30", "http://") or a cast ("x::text").
26 1 : export function findScriptReferences(names: readonly string[], text: string): ScriptReference[] {
27 1 : const references: ScriptReference[] = [];
28 1 : if (names.length === 0) {
29 1 : return references;
30 : }
31 1 : const lowerText = text.toLowerCase();
32 1 : const candidates = names
33 1 : .map(name => ({ name, lower: name.toLowerCase() }))
34 1 : .sort((l, r) => r.lower.length - l.lower.length);
35 1 : for (let at = text.indexOf(SCRIPT_REFERENCE_PREFIX); at >= 0; at = text.indexOf(SCRIPT_REFERENCE_PREFIX, at + 1)) {
36 1 : if (at > 0 && (NAME_CHAR.test(text[at - 1]) || text[at - 1] === SCRIPT_REFERENCE_PREFIX)) {
37 1 : continue;
38 : }
39 1 : const begin = at + SCRIPT_REFERENCE_PREFIX.length;
40 1 : const match = candidates.find(candidate => candidate.lower.length > 0
41 1 : && lowerText.startsWith(candidate.lower, begin)
42 1 : && !NAME_CHAR.test(text[begin + candidate.lower.length] ?? ''));
43 1 : if (match == null) {
44 1 : continue;
45 : }
46 1 : references.push({ from: at, to: begin + match.lower.length, name: match.name });
47 1 : at = begin + match.lower.length - 1;
48 1 : }
49 1 : return references;
50 1 : }
51 :
52 : /// The distinct names of the scripts that a text references, in the order of their first reference
53 1 : export function collectReferencedScripts(names: readonly string[], text: string): string[] {
54 1 : return [...new Set(findScriptReferences(names, text).map(reference => reference.name))];
55 1 : }
|