Skip to main content

rpfm_extensions/lua/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Knowledge about the Lua scripting API of the games.
12//!
13//! The API description is not shipped with RPFM. It's built at runtime from the scripting documentation
14//! bundled with the Assembly Kit (`documentation/script/script/`), which has two formats:
15//!
16//! - Per-page docs (`campaign/*.html`, `battle/*.html`, `frontend/*.html`): one `<dl class="function">`
17//!   block per function, with a typed signature, a parameter table and a list of return values.
18//! - `scripting_doc.html`: the game interfaces (`FACTION_SCRIPT_INTERFACE`, ...) with the return type of
19//!   each of their methods, and the context accessors available on each event.
20//!
21//! The docs don't list every event, nor what the vanilla scripts define beyond the documented libraries,
22//! so both are completed from the vanilla scripts in the dependencies cache. The [`check`] submodule uses
23//! this description to check scripts.
24
25use getset::Getters;
26use rayon::prelude::*;
27use regex::Regex;
28
29use std::collections::{BTreeSet, HashMap};
30use std::fs;
31use std::path::Path;
32use std::sync::LazyLock;
33
34use rpfm_lib::error::{RLibError, Result};
35use rpfm_lib::files::ContainerPath;
36
37use crate::dependencies::Dependencies;
38
39use self::check::LuaDefinitions;
40
41pub mod check;
42pub mod harness;
43#[cfg(test)] mod tests;
44
45/// Path of the scripting documentation, relative to the root folder of the Assembly Kit.
46pub const ASSEMBLY_KIT_SCRIPT_DOCS_PATH: &str = "documentation/script/script";
47
48/// File within the scripting documentation describing game interfaces and events.
49const SCRIPTING_DOC_FILE: &str = "scripting_doc.html";
50
51/// Interface methods the docs describe as answering yes or no, but document with a non-boolean return type.
52const BOOLEAN_RETURN_DOC_FIXES: [(&str, &str); 3] = [
53    ("CHARACTER_DETAILS_SCRIPT_INTERFACE", "has_trait"),
54    ("CHARACTER_SCRIPT_INTERFACE", "has_trait"),
55    ("FACTION_SCRIPT_INTERFACE", "was_confederated"),
56];
57
58/// Folder of the vanilla scripts, without trailing slash, as dependency lookups add it.
59const VANILLA_SCRIPTS_FOLDER: &str = "script";
60
61/// Vanilla script declaring one table per event the game can trigger.
62const VANILLA_EVENTS_SCRIPT: &str = "script/events.lua";
63
64static TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]*>").expect("valid regex"));
65static ENTITY_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);").expect("valid regex"));
66static INTERFACE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[A-Z][A-Z0-9_]*_SCRIPT_INTERFACE").expect("valid regex"));
67static IDENTIFIER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").expect("valid regex"));
68static SIGNATURE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(?s)<h3 class="function_name">(.*?)</h3>"#).expect("valid regex"));
69static PARAMETER_TABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(?s)<table class="parameter_list">(.*?)</table>"#).expect("valid regex"));
70static TABLE_ROW_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<tr>(.*?)</tr>").expect("valid regex"));
71static TABLE_CELL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<td>(.*?)</td>").expect("valid regex"));
72static DB_TABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<code>([a-z0-9_]+)</code>\s*(?:database\s+)?table\b").expect("valid regex"));
73static RETURNS_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<h4>Returns:</h4>\s*<ol>(.*?)</ol>").expect("valid regex"));
74static LIST_ITEM_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<li>(.*?)</li>").expect("valid regex"));
75static CODE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<code>(.*?)</code>").expect("valid regex"));
76static EVENT_ACCESSOR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)Function Name: ([A-Za-z0-9_]+)</dd>\s*<dd>Interface: (.*?)</dd>(?:\s*<dd>Description: (.*?)</dd>)?").expect("valid regex"));
77static DESCRIPTION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(?s)</dt>\s*<dd>(.*?)(?:<h4>|<p class="file_comment">|</dd>)"#).expect("valid regex"));
78static OPTIONAL_DEFAULT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<i>\s*optional, default value=(.*?)</i>").expect("valid regex"));
79static LINE_BREAK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)<br\s*/?>").expect("valid regex"));
80static INTERFACE_DESCRIPTION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<dd>Description: (.*?)</dd>").expect("valid regex"));
81static INTERFACE_PARAMETERS_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<dd>Parameters: (.*?)</dd>").expect("valid regex"));
82static INTERFACE_FUNCTION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(?s)<dd>Function: <a name="[^"]*">([A-Za-z0-9_]+)</a></dd>(.*?)(?:<br>|$)"#).expect("valid regex"));
83static INTERFACE_RETURN_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)<dd>Return: (.*?)</dd>").expect("valid regex"));
84static EVENT_TABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)^([A-Za-z0-9_]+)\s*=\s*\{\s*\}").expect("valid regex"));
85
86//---------------------------------------------------------------------------//
87//                              Enum & Structs
88//---------------------------------------------------------------------------//
89
90/// Game environment in which a script runs.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
92pub enum LuaEnvironment {
93    Campaign,
94    Battle,
95    Frontend,
96}
97
98/// How a documented function is called.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum LuaCallStyle {
101
102    /// `function(...)`.
103    Global,
104
105    /// `owner:function(...)`.
106    Method,
107
108    /// `owner.function(...)`.
109    Field,
110}
111
112/// Type of a value, as described by the docs.
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub enum LuaType {
115
116    /// The docs don't specify a usable type.
117    Any,
118    Nil,
119    Boolean,
120    Number,
121    String,
122    Table,
123    Function,
124
125    /// A game interface, like `FACTION_SCRIPT_INTERFACE`.
126    Interface(String),
127
128    /// A named object type, like `faction` or `battle_unit`. It may or may not match an interface or an owner.
129    Object(String),
130}
131
132/// A parameter of a documented function.
133#[derive(Clone, Debug, PartialEq, Getters)]
134#[getset(get = "pub")]
135pub struct LuaParameter {
136
137    /// Name of the parameter, as written in the docs.
138    name: String,
139
140    /// Type of the parameter.
141    lua_type: LuaType,
142
143    /// If the parameter can be omitted.
144    optional: bool,
145
146    /// If the parameter takes any amount of values (`...`).
147    variadic: bool,
148
149    /// DB table, with the `_tables` suffix, whose keys this parameter expects.
150    db_table: Option<String>,
151
152    /// Description of the parameter.
153    description: String,
154}
155
156/// A value returned by a documented function.
157#[derive(Clone, Debug, PartialEq, Getters)]
158#[getset(get = "pub")]
159pub struct LuaReturn {
160
161    /// Type of the value.
162    lua_type: LuaType,
163
164    /// Description of the value.
165    description: String,
166}
167
168/// A context accessor of an event, like `context:faction()`.
169#[derive(Clone, Debug, PartialEq, Getters)]
170#[getset(get = "pub")]
171pub struct LuaAccessor {
172
173    /// Type of the value the accessor returns.
174    lua_type: LuaType,
175
176    /// Description of the accessor.
177    description: String,
178}
179
180/// A documented function.
181#[derive(Clone, Debug, PartialEq, Getters)]
182#[getset(get = "pub")]
183pub struct LuaFunction {
184
185    /// Name of the function.
186    name: String,
187
188    /// How the function is called.
189    call_style: LuaCallStyle,
190
191    /// Signature of the function, as written in the docs.
192    signature: String,
193
194    /// Description of the function. Paragraphs are separated by line breaks.
195    description: String,
196
197    /// Parameters of the function. `None` if the docs don't describe them in a usable way.
198    parameters: Option<Vec<LuaParameter>>,
199
200    /// Returned values. Empty if the function returns nothing.
201    returns: Vec<LuaReturn>,
202
203    /// Environments whose docs include this function. Empty for interface methods, which are not tied to one.
204    environments: BTreeSet<LuaEnvironment>,
205}
206
207/// Description of the Lua scripting API of a game.
208#[derive(Clone, Debug, Default, PartialEq, Getters)]
209#[getset(get = "pub")]
210pub struct LuaApi {
211
212    /// Functions by owner, then by name. The owner is the part before `:` or `.` in a call
213    /// (`cm`, `common`, `FACTION_SCRIPT_INTERFACE`, ...), and the empty string for global functions.
214    owners: HashMap<String, HashMap<String, LuaFunction>>,
215
216    /// Context accessors of each event, by event name, then by accessor name.
217    events: HashMap<String, HashMap<String, LuaAccessor>>,
218
219    /// Members and custom events defined by the vanilla scripts.
220    script_definitions: LuaDefinitions,
221}
222
223/// Something in a script the docs describe, used to show its docs on hover.
224#[derive(Clone, Debug, PartialEq, Eq, Hash)]
225pub enum LuaHoverTarget {
226
227    /// A documented function. Contains its owner (empty for globals) and its name.
228    Function(String, String),
229
230    /// A context accessor. Contains the event and the accessor.
231    Accessor(String, String),
232
233    /// An event.
234    Event(String),
235}
236
237//---------------------------------------------------------------------------//
238//                             Implementations
239//---------------------------------------------------------------------------//
240
241impl LuaEnvironment {
242
243    /// Folder of the scripting docs holding the pages of this environment.
244    pub fn docs_folder(&self) -> &'static str {
245        match self {
246            Self::Campaign => "campaign",
247            Self::Battle => "battle",
248            Self::Frontend => "frontend",
249        }
250    }
251}
252
253impl LuaType {
254
255    /// This function returns the name of the type, for showing it to users.
256    pub fn name(&self) -> &str {
257        match self {
258            Self::Any => "any",
259            Self::Nil => "nil",
260            Self::Boolean => "boolean",
261            Self::Number => "number",
262            Self::String => "string",
263            Self::Table => "table",
264            Self::Function => "function",
265            Self::Interface(name) | Self::Object(name) => name,
266        }
267    }
268
269    /// This function maps a type name, as written in the docs, to a type.
270    ///
271    /// # Arguments
272    ///
273    /// * `doc_name` - Type name from the docs, like `boolean`, `card32` or `FACTION_SCRIPT_INTERFACE`.
274    ///
275    /// # Returns
276    ///
277    /// The matching type, or [`LuaType::Any`] if the name doesn't describe one.
278    pub fn from_doc_name(doc_name: &str) -> Self {
279        let doc_name = doc_name.trim();
280        if let Some(interface) = INTERFACE_REGEX.find(doc_name) {
281            return Self::Interface(interface.as_str().to_owned());
282        }
283
284        let lower = doc_name.to_lowercase();
285        let first_word = lower.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
286            .find(|word| !word.is_empty())
287            .unwrap_or_default();
288
289        match first_word {
290            "" | "nil" | "void" | "none" => Self::Nil,
291            "bool" | "boolean" | "logical" => Self::Boolean,
292            "number" | "integer" | "int" | "int32" | "card16" | "card32" | "float" | "float32" | "positive" | "index" | "distance" | "proportion" => Self::Number,
293            "string" if lower.contains("table") => Self::Table,
294            "string" => Self::String,
295            "table" | "list" | "lists" | "lua" | "ca_std" | "sorted" | "accumulated" => Self::Table,
296            "function" | "iterator" => Self::Function,
297            "object" | "value" | "variable" | "userdata" | "address" | "any" => Self::Any,
298
299            // Ranges, like "100 >= float >= 0".
300            _ if first_word.starts_with(|c: char| c.is_ascii_digit()) => Self::Number,
301            _ if lower.starts_with('(') => Self::Any,
302            _ => Self::Object(first_word.to_owned()),
303        }
304    }
305}
306
307impl LuaApi {
308
309    /// This function builds the API description from the scripting docs of an Assembly Kit.
310    ///
311    /// # Arguments
312    ///
313    /// * `docs_path` - Path of the scripting docs. See [`ASSEMBLY_KIT_SCRIPT_DOCS_PATH`].
314    ///
315    /// # Returns
316    ///
317    /// The API described by the docs.
318    ///
319    /// # Errors
320    ///
321    /// Returns [`RLibError::AssemblyKitNotFound`] if the docs folder doesn't exist, or an IO error if a page can't be read.
322    pub fn from_assembly_kit(docs_path: &Path) -> Result<Self> {
323        if !docs_path.is_dir() {
324            return Err(RLibError::AssemblyKitNotFound);
325        }
326
327        let mut api = Self::default();
328        for environment in [LuaEnvironment::Campaign, LuaEnvironment::Battle, LuaEnvironment::Frontend] {
329            let folder = docs_path.join(environment.docs_folder());
330            if !folder.is_dir() {
331                continue;
332            }
333
334            for entry in fs::read_dir(&folder)? {
335                let path = entry?.path();
336                if path.extension().is_some_and(|extension| extension == "html") {
337                    let page = fs::read(&path)?;
338                    api.add_page(&String::from_utf8_lossy(&page), environment);
339                }
340            }
341        }
342
343        let scripting_doc_path = docs_path.join(SCRIPTING_DOC_FILE);
344        if scripting_doc_path.is_file() {
345            let scripting_doc = fs::read(&scripting_doc_path)?;
346            api.add_scripting_doc(&String::from_utf8_lossy(&scripting_doc));
347        }
348
349        Ok(api)
350    }
351
352    /// This function completes the API with the vanilla scripts from the dependencies cache.
353    ///
354    /// Events from `script/events.lua` missing from the docs are added without accessors, and what the
355    /// other vanilla scripts define is added to [`LuaApi::script_definitions`].
356    ///
357    /// # Arguments
358    ///
359    /// * `dependencies` - Dependencies cache with the vanilla files loaded.
360    pub fn add_vanilla_scripts(&mut self, dependencies: &Dependencies) {
361        let scripts = vanilla_scripts(dependencies);
362        for (path, source) in &scripts {
363            if path == VANILLA_EVENTS_SCRIPT {
364                self.add_events_script(source);
365            }
366        }
367
368        self.script_definitions = scripts.par_iter()
369            .filter(|(path, _)| path != VANILLA_EVENTS_SCRIPT)
370            .fold(LuaDefinitions::default, |mut definitions, (_, source)| {
371                definitions.add_script(source);
372                definitions
373            })
374            .reduce(LuaDefinitions::default, |mut definitions, other| {
375                definitions.extend(other);
376                definitions
377            });
378    }
379
380    /// This function returns the docs of something in a script, formatted for a tooltip.
381    ///
382    /// # Arguments
383    ///
384    /// * `target` - What to return the docs of.
385    ///
386    /// # Returns
387    ///
388    /// The docs as Qt rich text, or `None` if the target is not documented.
389    pub fn hover_html(&self, target: &LuaHoverTarget) -> Option<String> {
390        match target {
391            LuaHoverTarget::Function(owner, name) => {
392                let function = self.function(owner, name)?;
393                let mut html = format!("<p><code>{}</code></p>", html_escape(function.signature()));
394                if !function.description().is_empty() {
395                    html.push_str(&format!("<p>{}</p>", html_escape(function.description()).replace('\n', "<br/>")));
396                }
397
398                let parameters = function.parameters().as_deref().unwrap_or_default();
399                if !parameters.is_empty() {
400                    html.push_str("<p><b>Parameters:</b></p><ul>");
401                    for parameter in parameters {
402                        html.push_str(&format!("<li><code>{}</code>: {}</li>", html_escape(parameter.name()), html_escape(parameter.description())));
403                    }
404                    html.push_str("</ul>");
405                }
406
407                if !function.returns().is_empty() {
408                    html.push_str("<p><b>Returns:</b></p><ul>");
409                    for returned in function.returns() {
410                        html.push_str(&format!("<li><code>{}</code> {}</li>", html_escape(returned.lua_type().name()), html_escape(returned.description())));
411                    }
412                    html.push_str("</ul>");
413                }
414
415                Some(html)
416            }
417
418            LuaHoverTarget::Accessor(event, name) => {
419                let accessor = self.events.get(event)?.get(name)?;
420                let mut html = format!("<p><code>{}:{}()</code> &rarr; <code>{}</code></p>", html_escape(event), html_escape(name), html_escape(accessor.lua_type().name()));
421                if !accessor.description().is_empty() {
422                    html.push_str(&format!("<p>{}</p>", html_escape(accessor.description())));
423                }
424
425                Some(html)
426            }
427
428            LuaHoverTarget::Event(event) => {
429                let accessors = self.events.get(event)?;
430                let mut html = format!("<p>Event <code>{}</code></p>", html_escape(event));
431                if !accessors.is_empty() {
432                    let mut names = accessors.keys().collect::<Vec<_>>();
433                    names.sort();
434
435                    html.push_str("<p><b>Context:</b></p><ul>");
436                    for name in names {
437                        let accessor = &accessors[name];
438                        html.push_str(&format!("<li><code>context:{}()</code> &rarr; <code>{}</code> {}</li>", html_escape(name), html_escape(accessor.lua_type().name()), html_escape(accessor.description())));
439                    }
440                    html.push_str("</ul>");
441                }
442
443                Some(html)
444            }
445        }
446    }
447
448    /// This function returns a documented function.
449    ///
450    /// # Arguments
451    ///
452    /// * `owner` - Owner of the function. Empty for global functions.
453    /// * `name` - Name of the function.
454    ///
455    /// # Returns
456    ///
457    /// The function, if it's documented.
458    pub fn function(&self, owner: &str, name: &str) -> Option<&LuaFunction> {
459        self.owners.get(owner).and_then(|functions| functions.get(name))
460    }
461
462    /// This function adds the functions of one per-page doc to the API.
463    ///
464    /// Functions already known from another environment's page only get the new environment added.
465    ///
466    /// # Arguments
467    ///
468    /// * `page` - HTML of the page.
469    /// * `environment` - Environment whose docs the page belongs to.
470    pub(crate) fn add_page(&mut self, page: &str, environment: LuaEnvironment) {
471        for block in page.split(r#"<dl class="function">"#).skip(1) {
472            let Some((owner, mut function)) = parse_function_block(block) else {
473                continue;
474            };
475
476            let functions = self.owners.entry(owner).or_default();
477            match functions.get_mut(&function.name) {
478                Some(existing) => { existing.environments.insert(environment); },
479                None => {
480                    function.environments.insert(environment);
481                    functions.insert(function.name.to_owned(), function);
482                }
483            }
484        }
485
486    }
487
488    /// This function adds the events declared in `script/events.lua` that are not already known.
489    ///
490    /// # Arguments
491    ///
492    /// * `source` - Code of `script/events.lua`.
493    pub(crate) fn add_events_script(&mut self, source: &str) {
494        for captures in EVENT_TABLE_REGEX.captures_iter(source) {
495            self.events.entry(captures[1].to_owned()).or_default();
496        }
497    }
498
499    /// This function adds the game interfaces and events described in `scripting_doc.html` to the API.
500    ///
501    /// # Arguments
502    ///
503    /// * `scripting_doc` - HTML of `scripting_doc.html`.
504    pub(crate) fn add_scripting_doc(&mut self, scripting_doc: &str) {
505        let events_start = scripting_doc.find(">Event Functions<");
506        let interfaces_start = scripting_doc.find(">Interface Functions<");
507
508        if let Some(events_start) = events_start {
509            let events_end = interfaces_start.filter(|end| *end > events_start).unwrap_or(scripting_doc.len());
510            for block in scripting_doc[events_start..events_end].split(r#"<h4><a name=""#).skip(1) {
511                let Some(event_name) = block.split('"').next() else {
512                    continue;
513                };
514
515                let accessors = EVENT_ACCESSOR_REGEX.captures_iter(block)
516                    .map(|captures| {
517                        let accessor = LuaAccessor {
518                            lua_type: LuaType::from_doc_name(&html_to_text(&captures[2])),
519                            description: captures.get(3).map(|description| html_to_text(description.as_str())).unwrap_or_default(),
520                        };
521
522                        (captures[1].to_owned(), accessor)
523                    })
524                    .collect();
525
526                self.events.insert(event_name.to_owned(), accessors);
527            }
528        }
529
530        if let Some(interfaces_start) = interfaces_start {
531            for block in scripting_doc[interfaces_start..].split(r#"<h4><a name=""#).skip(1) {
532                let Some(interface_name) = block.split('"').next() else {
533                    continue;
534                };
535
536                let functions = self.owners.entry(interface_name.to_owned()).or_default();
537                for captures in INTERFACE_FUNCTION_REGEX.captures_iter(block) {
538                    let name = &captures[1];
539                    let details = &captures[2];
540                    let returns = INTERFACE_RETURN_REGEX.captures(details)
541                        .map(|return_captures| parse_returns([LuaReturn {
542                            lua_type: LuaType::from_doc_name(&html_to_text(&return_captures[1])),
543                            description: String::new(),
544                        }]))
545                        .unwrap_or_default();
546
547                    // The parameters are either an example call, like `at_war_with(faction)`, or just the types, like `positive int`.
548                    let parameters = INTERFACE_PARAMETERS_REGEX.captures(details).map(|parameters| html_to_text(&parameters[1])).unwrap_or_default();
549                    let signature = if parameters.starts_with(&format!("{name}(")) {
550                        format!("{interface_name}:{parameters}")
551                    } else {
552                        format!("{interface_name}:{name}({parameters})")
553                    };
554
555                    let function = LuaFunction {
556                        name: name.to_owned(),
557                        call_style: LuaCallStyle::Method,
558                        signature,
559                        description: INTERFACE_DESCRIPTION_REGEX.captures(details).map(|description| html_to_text(&description[1])).unwrap_or_default(),
560                        parameters: None,
561                        returns,
562                        environments: BTreeSet::new(),
563                    };
564
565                    functions.insert(function.name.to_owned(), function);
566                }
567            }
568        }
569
570        for (owner, name) in BOOLEAN_RETURN_DOC_FIXES {
571            if let Some(function) = self.owners.get_mut(owner).and_then(|functions| functions.get_mut(name)) {
572                function.returns = vec![LuaReturn { lua_type: LuaType::Boolean, description: String::new() }];
573            }
574        }
575    }
576}
577
578/// This function returns the vanilla Lua scripts from the dependencies cache.
579///
580/// # Arguments
581///
582/// * `dependencies` - Dependencies cache with the vanilla files loaded.
583///
584/// # Returns
585///
586/// The path and code of each script.
587pub fn vanilla_scripts(dependencies: &Dependencies) -> Vec<(String, String)> {
588    dependencies.files_by_path(&[ContainerPath::Folder(VANILLA_SCRIPTS_FOLDER.to_owned())], true, false, false)
589        .into_par_iter()
590        .filter(|(path, _)| path.ends_with(".lua"))
591        .filter_map(|(path, file)| {
592
593            // Vanilla files are loaded from disk on demand, so work on a copy to not need mutable access to the cache.
594            let mut file = file.clone();
595            file.load().ok()?;
596            Some((path, String::from_utf8_lossy(file.cached().ok()?).to_string()))
597        })
598        .collect()
599}
600
601//---------------------------------------------------------------------------//
602//                              Parsing helpers
603//---------------------------------------------------------------------------//
604
605/// This function parses a `<dl class="function">` block of a per-page doc.
606///
607/// # Arguments
608///
609/// * `block` - HTML of the block, without the opening `<dl>` tag.
610///
611/// # Returns
612///
613/// The owner of the function and the function, or `None` if the block doesn't describe a function.
614fn parse_function_block(block: &str) -> Option<(String, LuaFunction)> {
615    if !block.contains(r#"name="function:"#) {
616        return None;
617    }
618
619    let signature = tidy_signature(&html_to_text(&SIGNATURE_REGEX.captures(block)?[1]));
620    let (head, arguments) = signature.split_once('(')?;
621    let arguments = arguments.rsplit_once(')').map(|(arguments, _)| arguments).unwrap_or(arguments).to_owned();
622
623    let head = head.trim();
624    let (owner, name, call_style) = match head.rfind([':', '.']) {
625        Some(position) if head[position..].starts_with(':') => (&head[..position], &head[position + 1..], LuaCallStyle::Method),
626        Some(position) => (&head[..position], &head[position + 1..], LuaCallStyle::Field),
627        None => ("", head, LuaCallStyle::Global),
628    };
629
630    if !IDENTIFIER_REGEX.is_match(name) || (!owner.is_empty() && !IDENTIFIER_REGEX.is_match(owner)) {
631        return None;
632    }
633
634    let owner = owner.to_owned();
635    let name = name.to_owned();
636
637    let mut parameters = parse_parameters(&arguments);
638
639    // The parameter table lists the parameters in signature order, starting at 1.
640    if let Some(table) = PARAMETER_TABLE_REGEX.captures(block) {
641        for row in TABLE_ROW_REGEX.captures_iter(&table[1]) {
642            let cells = TABLE_CELL_REGEX.captures_iter(&row[1]).map(|cell| cell[1].to_owned()).collect::<Vec<_>>();
643            let (Some(index), Some(description)) = (cells.first(), cells.get(2)) else {
644                continue;
645            };
646
647            let Some(parameter) = html_to_text(index).parse::<usize>().ok().and_then(|index| parameters.get_mut(index.checked_sub(1)?)) else {
648                continue;
649            };
650
651            match OPTIONAL_DEFAULT_REGEX.captures(description) {
652                Some(default) => {
653                    parameter.optional = true;
654                    let rest = html_to_text(&OPTIONAL_DEFAULT_REGEX.replace(description, ""));
655                    parameter.description = format!("(optional, default: {}) {rest}", html_to_text(&default[1])).trim_end().to_owned();
656                }
657                None => parameter.description = html_to_text(description),
658            }
659
660            if let Some(table_name) = DB_TABLE_REGEX.captures(description) {
661                let table_name = &table_name[1];
662                parameter.db_table = Some(if table_name.ends_with("_tables") { table_name.to_owned() } else { format!("{table_name}_tables") });
663            }
664        }
665    }
666
667    // Each returned value is its type in a `<code>` tag, followed by its description.
668    let returns = RETURNS_REGEX.captures(block)
669        .map(|returns| parse_returns(LIST_ITEM_REGEX.captures_iter(&returns[1])
670            .map(|item| match CODE_REGEX.captures(&item[1]) {
671                Some(code) => LuaReturn {
672                    lua_type: LuaType::from_doc_name(&html_to_text(&code[1])),
673                    description: html_to_text(&item[1][code.get(0).map_or(0, |code| code.end())..]),
674                },
675                None => LuaReturn { lua_type: LuaType::Any, description: html_to_text(&item[1]) },
676            })))
677        .unwrap_or_default();
678
679    let description = DESCRIPTION_REGEX.captures(block)
680        .map(|description| LINE_BREAK_REGEX.split(&description[1])
681            .map(html_to_text)
682            .filter(|line| !line.is_empty())
683            .collect::<Vec<_>>()
684            .join("\n"))
685        .unwrap_or_default();
686
687    let function = LuaFunction {
688        name,
689        call_style,
690        signature,
691        description,
692        parameters: Some(parameters),
693        returns,
694        environments: BTreeSet::new(),
695    };
696
697    Some((owner, function))
698}
699
700/// This function parses the argument list of a signature, like `string key, [number amount], ... values`.
701///
702/// # Arguments
703///
704/// * `arguments` - Text between the parentheses of the signature.
705///
706/// # Returns
707///
708/// The parameters, in order.
709fn parse_parameters(arguments: &str) -> Vec<LuaParameter> {
710    arguments.split(',')
711        .map(str::trim)
712        .filter(|argument| !argument.is_empty())
713        .map(|argument| {
714
715            // Optional parameters are wrapped in brackets, which may be nested across several arguments.
716            let optional = argument.contains('[');
717            let argument = argument.trim_matches(|c: char| c == '[' || c == ']' || c.is_whitespace());
718            let (doc_type, name) = argument.split_once(char::is_whitespace).unwrap_or((argument, ""));
719            let variadic = doc_type == "...";
720
721            LuaParameter {
722                name: name.trim().to_owned(),
723                lua_type: if variadic { LuaType::Any } else { LuaType::from_doc_name(doc_type) },
724                optional,
725                variadic,
726                db_table: None,
727                description: String::new(),
728            }
729        })
730        .collect()
731}
732
733/// This function turns the documented returns of a function into its list of returned values.
734///
735/// # Arguments
736///
737/// * `returns` - Documented returns, in order.
738///
739/// # Returns
740///
741/// The returned values, without the `nil` entries used by the docs to mean "returns nothing".
742fn parse_returns(returns: impl IntoIterator<Item = LuaReturn>) -> Vec<LuaReturn> {
743    returns.into_iter().filter(|returned| returned.lua_type != LuaType::Nil).collect()
744}
745
746/// This function removes the spaces left around punctuation when turning a signature's markup into text.
747///
748/// # Arguments
749///
750/// * `signature` - Signature as text, like `cm:f( string key , [ number x ])`.
751///
752/// # Returns
753///
754/// The signature with normal spacing, like `cm:f(string key, [number x])`.
755fn tidy_signature(signature: &str) -> String {
756    signature.replace(" ,", ",")
757        .replace("( ", "(")
758        .replace(" )", ")")
759        .replace("[ ", "[")
760        .replace(" ]", "]")
761}
762
763/// This function escapes text so it can be embedded in HTML.
764///
765/// # Arguments
766///
767/// * `text` - Text to escape.
768///
769/// # Returns
770///
771/// The escaped text.
772fn html_escape(text: &str) -> String {
773    text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
774}
775
776/// This function turns a fragment of HTML into plain text.
777///
778/// # Arguments
779///
780/// * `html` - HTML fragment.
781///
782/// # Returns
783///
784/// The text of the fragment, with tags removed, entities decoded and whitespace collapsed.
785fn html_to_text(html: &str) -> String {
786    let text = TAG_REGEX.replace_all(html, " ");
787    let text = ENTITY_REGEX.replace_all(&text, |captures: &regex::Captures| {
788        let entity = &captures[1];
789        let decoded = match entity {
790            "amp" => Some('&'),
791            "lt" => Some('<'),
792            "gt" => Some('>'),
793            "quot" => Some('"'),
794            "apos" => Some('\''),
795            "nbsp" | "emsp" | "ensp" | "thinsp" => Some(' '),
796            _ => entity.strip_prefix("#x").map_or_else(
797                || entity.strip_prefix('#').and_then(|code| code.parse::<u32>().ok()),
798                |code| u32::from_str_radix(code, 16).ok()
799            ).and_then(char::from_u32),
800        };
801
802        decoded.map_or_else(|| captures[0].to_owned(), String::from)
803    });
804
805    text.split_whitespace().collect::<Vec<_>>().join(" ")
806}