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