use std::collections::HashMap;
use std::fmt::Write as _;
use crate::context::Context;
use crate::models::domain::{ApiView, Class, ClassLike, Enum, Enumerator, Function, TyName};
use crate::{conv, special_cases, util};
type CowStr = std::borrow::Cow<'static, str>;
macro_rules! write_str {
($out:expr, $($arg:tt)*) => {
write!($out, $($arg)*).expect("writing to String should not fail")
};
}
pub fn import_docs(
description: &str,
surrounding_class: Option<&Class>,
ctx: &Context,
view: &ApiView,
) -> String {
DocImporter::new(description, surrounding_class, ctx, view).import()
}
pub fn import_function_docs(fun: &dyn Function, ctx: &Context, view: &ApiView) -> Option<String> {
let doc = fun.common().description.as_ref()?;
if doc.is_empty() {
return None;
}
let surrounding_class_name = fun.surrounding_class();
let surrounding_class = surrounding_class_name.and_then(|name| view.find_engine_class(name));
let imported_doc = import_docs(doc, surrounding_class, ctx, view);
Some(imported_doc)
}
fn matches_primitive_type(ty: &str) -> bool {
matches!(ty, "int" | "float" | "bool")
}
fn matches_ignored_links(class: &str) -> bool {
class == "@GDScript"
}
#[derive(Copy, Clone)]
struct ParseMode {
double_newlines: bool,
allow_type_links: bool,
allow_tags: bool,
}
impl ParseMode {
const TOP: Self = Self {
double_newlines: true,
allow_type_links: true,
allow_tags: true,
};
const CODE: Self = Self {
double_newlines: false,
allow_type_links: false,
allow_tags: false,
};
fn inherit_for(self, allow_inner: bool) -> Self {
if allow_inner {
Self {
double_newlines: self.double_newlines,
allow_type_links: self.allow_type_links,
allow_tags: true,
}
} else {
Self::CODE
}
}
}
enum WrappedTag {
Render {
open: &'static str,
close: &'static str,
prefix: &'static str,
suffix: &'static str,
allow_inner: bool,
},
Skip {
open: &'static str,
close: &'static str,
},
}
impl WrappedTag {
fn open(&self) -> &'static str {
match self {
Self::Render { open, .. } | Self::Skip { open, .. } => open,
}
}
}
#[rustfmt::skip]
const WRAPPED_TAGS: &[WrappedTag] = &[
WrappedTag::Render { open: "[b]", close: "[/b]", prefix: "**", suffix: "**", allow_inner: true },
WrappedTag::Render { open: "[i]", close: "[/i]", prefix: "_", suffix: "_", allow_inner: true },
WrappedTag::Render { open: "[kbd]", close: "[/kbd]", prefix: "`", suffix: "`", allow_inner: true },
WrappedTag::Render { open: "[code skip-lint]", close: "[/code]", prefix: "`", suffix: "`", allow_inner: false },
WrappedTag::Render { open: "[code]", close: "[/code]", prefix: "`", suffix: "`", allow_inner: false },
WrappedTag::Render { open: "[codeblock]", close: "[/codeblock]", prefix: "```gdscript", suffix: "```", allow_inner: false },
WrappedTag::Render { open: "[gdscript]", close: "[/gdscript]", prefix: "```gdscript", suffix: "```", allow_inner: false },
WrappedTag::Skip { open: "[csharp]", close: "[/csharp]" },
];
struct DocImporter<'d> {
doc: &'d str,
pos: usize,
surrounding_class: Option<&'d Class>,
ctx: &'d Context,
view: &'d ApiView<'d>,
path_cache: HashMap<String, String>,
}
impl<'d> DocImporter<'d> {
fn new(
doc: &'d str,
surrounding_class: Option<&'d Class>,
ctx: &'d Context,
view: &'d ApiView<'d>,
) -> Self {
Self {
doc,
pos: 0,
surrounding_class,
ctx,
view,
path_cache: HashMap::new(),
}
}
fn import(mut self) -> String {
let mut out = String::with_capacity(self.doc.len() * 4);
let ok = self.parse_until(&mut out, None, ParseMode::TOP);
debug_assert!(ok, "top-level parse_until without closing tag must succeed");
out
}
fn checkpoint(&self, out: &str) -> (usize, usize) {
(self.pos, out.len())
}
fn rollback(&mut self, out: &mut String, cp: (usize, usize)) {
self.pos = cp.0;
out.truncate(cp.1);
}
fn skip_past(&mut self, target: &str) -> bool {
if let Some(offset) = self.doc[self.pos..].find(target) {
self.pos += offset + target.len();
true
} else {
false
}
}
fn parse_until(
&mut self,
out: &mut String,
closing_tag: Option<&str>,
mode: ParseMode,
) -> bool {
let cp = self.checkpoint(out);
while self.pos < self.doc.len() {
if let Some(close) = closing_tag
&& self.remaining().starts_with(close)
{
self.pos += close.len();
return true;
}
if mode.allow_tags && self.remaining().starts_with('[') && self.try_parse_tag(out, mode)
{
continue;
}
let ch = self.remaining().chars().next().unwrap();
self.pos += ch.len_utf8();
if ch == '\n' && mode.double_newlines {
out.push_str("\n\n");
} else {
out.push(ch);
}
}
if closing_tag.is_none() {
true
} else {
self.rollback(out, cp);
false
}
}
fn try_parse_tag(&mut self, out: &mut String, mode: ParseMode) -> bool {
for tag in WRAPPED_TAGS {
if self.try_wrapped_tag(out, tag, mode) {
return true;
}
}
if self.try_url_tag(out, mode)
|| self.try_codeblocks_tag(out)
|| self.try_codeblock_lang_tag(out)
{
return true;
}
if starts_with_known_tag(self.remaining()) {
return false;
}
self.try_markdown_link(out) || self.try_bracket_link(out, mode.allow_type_links)
}
fn try_wrapped_tag(&mut self, out: &mut String, tag: &WrappedTag, mode: ParseMode) -> bool {
if !self.remaining().starts_with(tag.open()) {
return false;
}
let cp = self.checkpoint(out);
self.pos += tag.open().len();
match *tag {
WrappedTag::Render {
close,
prefix,
suffix,
allow_inner,
..
} => {
out.push_str(prefix);
if !self.parse_until(out, Some(close), mode.inherit_for(allow_inner)) {
self.rollback(out, cp);
return false;
}
out.push_str(suffix);
}
WrappedTag::Skip { close, .. } => {
if !self.skip_past(close) {
self.rollback(out, cp);
return false;
}
if out.ends_with('\n') {
out.pop();
}
}
}
true
}
fn try_consume_attr_opener(&mut self, prefix: &str) -> Option<(usize, usize)> {
let remaining = self.remaining();
if !remaining.starts_with(prefix) {
return None;
}
let end = remaining.find(']')?;
let value_start = self.pos + prefix.len();
let value_end = self.pos + end;
self.pos += end + 1;
Some((value_start, value_end))
}
fn try_url_tag(&mut self, out: &mut String, mode: ParseMode) -> bool {
const PREFIX: &str = "[url=";
const SUFFIX: &str = "[/url]";
let cp = self.checkpoint(out);
let Some((url_start, url_end)) = self.try_consume_attr_opener(PREFIX) else {
return false;
};
out.push('[');
if !self.parse_until(out, Some(SUFFIX), mode) {
self.rollback(out, cp);
return false;
}
write_str!(out, "]({})", &self.doc[url_start..url_end]);
true
}
fn try_codeblocks_tag(&mut self, out: &mut String) -> bool {
const OPENING_TAG: &str = "[codeblocks]";
const CLOSING_TAG: &str = "[/codeblocks]";
if !self.remaining().starts_with(OPENING_TAG) {
return false;
}
let cp = self.checkpoint(out);
self.pos += OPENING_TAG.len();
let inner = ParseMode {
double_newlines: false,
allow_type_links: true,
allow_tags: true,
};
if !self.parse_until(out, Some(CLOSING_TAG), inner) {
self.rollback(out, cp);
return false;
}
true
}
fn try_codeblock_lang_tag(&mut self, out: &mut String) -> bool {
const PREFIX: &str = "[codeblock lang=";
const SUFFIX: &str = "[/codeblock]";
let cp = self.checkpoint(out);
let Some((lang_start, lang_end)) = self.try_consume_attr_opener(PREFIX) else {
return false;
};
{
let lang = &self.doc[lang_start..lang_end];
write_str!(out, "```{lang}");
}
if !self.parse_until(out, Some(SUFFIX), ParseMode::CODE) {
self.rollback(out, cp);
return false;
}
out.push_str("```");
true
}
fn try_markdown_link(&mut self, out: &mut String) -> bool {
let remaining = self.remaining();
if !remaining.starts_with('[') {
return false;
}
let Some(end_of_text) = remaining.find(']') else {
return false;
};
let after_text = &remaining[end_of_text + 1..];
if !after_text.starts_with("(http") {
return false;
}
let Some(end_of_target) = after_text.find(')') else {
return false;
};
let len = end_of_text + 1 + end_of_target + 1;
out.push_str(&remaining[..len]);
self.pos += len;
true
}
fn try_bracket_link(&mut self, out: &mut String, allow_type_links: bool) -> bool {
let remaining = self.remaining();
if !remaining.starts_with('[') {
return false;
}
let Some(end) = remaining.find(']') else {
return false;
};
let whole = &remaining[..=end];
let content = &remaining[1..end];
if let Some(param_name) = content.strip_prefix("param ")
&& is_ident_like(param_name)
{
self.pos += whole.len();
write_str!(out, "`{param_name}`");
return true;
}
if let Some(method_path) = content.strip_prefix("method ") {
self.pos += whole.len();
self.write_method_link(out, whole, method_path);
return true;
}
if let Some(signal_name) = content.strip_prefix("signal ") {
self.pos += whole.len();
write_code_span(out, signal_name);
return true;
}
if let Some(annotation) = content.strip_prefix("annotation ") {
self.pos += whole.len();
write_code_span(out, annotation);
return true;
}
if let Some(constant) = content.strip_prefix("constant ") {
self.pos += whole.len();
self.write_constant_link(out, constant);
return true;
}
if let Some(ty_name) = content.strip_prefix("constructor ") {
self.pos += whole.len();
out.push('`');
out.push_str(ty_name);
out.push_str("()`");
return true;
}
if is_escaped_role(content) {
self.pos += whole.len();
write_str!(out, "\\{whole}");
return true;
}
if allow_type_links && is_type_link(content) {
self.pos += whole.len();
self.write_type_link(out, content);
return true;
}
false
}
fn write_type_link(&mut self, out: &mut String, ty_name: &str) {
if matches_ignored_links(ty_name) {
out.push_str(ty_name);
} else if matches_primitive_type(ty_name) {
write_code_span(out, ty_name);
} else if let Some(hardcoded) = matches_hardcoded_type(ty_name) {
out.push_str(hardcoded);
} else if !self.ctx.is_builtin(ty_name) && special_cases::is_class_deleted_str(ty_name) {
write_code_span(out, ty_name);
} else if self
.surrounding_class
.is_some_and(|c| c.name().godot_ty == ty_name)
{
write_code_span(out, ty_name);
} else {
let path = self.cached_class_rust_path(ty_name);
write_code_link(out, ty_name, path);
}
}
fn write_method_link(&mut self, out: &mut String, whole_match: &str, method_path: &str) {
if let Some(method_path) = convert_to_method_path(
method_path,
self.surrounding_class,
self.ctx,
self.view,
&mut self.path_cache,
) {
let (_, method_name) = method_path
.rsplit_once("::")
.expect("rsplit_once should return a method name");
write_str!(out, "[`{method_name}`][`{method_path}`]");
} else {
write_str!(out, "\\{whole_match}");
}
}
fn write_constant_link(&self, out: &mut String, godot_const_ref: &str) {
if let Some((class_godot_name, const_godot_name)) = godot_const_ref.split_once('.') {
if let Some((class, enum_, enumerator)) =
self.find_class_enum_constant(class_godot_name, const_godot_name)
{
let module = format!("crate::classes::{}", class.mod_name().rust_mod);
write_enum_const_link(out, &module, &enum_.name, &enumerator.name);
return;
}
if !class_godot_name.starts_with('@')
&& let Some((_decl_enum, variant)) =
self.ctx.find_notification_constant(const_godot_name)
{
let class_ty = TyName::from_godot(class_godot_name);
if self.view.find_engine_class(&class_ty).is_some() {
let enum_name = self.ctx.notification_enum_name(&class_ty).name;
write_enum_const_link(out, "crate::classes::notify", &enum_name, &variant);
return;
}
}
} else {
if let Some((decl_enum_ident, variant)) =
self.ctx.find_notification_constant(godot_const_ref)
{
let enum_ident = self
.surrounding_class
.map(|c| self.ctx.notification_enum_name(c.name()).name)
.unwrap_or_else(|| decl_enum_ident.clone());
write_enum_const_link(out, "crate::classes::notify", &enum_ident, &variant);
return;
}
if let Some(surrounding_class) = self.surrounding_class
&& let Some((class, enum_, enumerator)) =
self.find_class_enum_constant_in_hierarchy(surrounding_class, godot_const_ref)
{
let module = format!("crate::classes::{}", class.mod_name().rust_mod);
write_enum_const_link(out, &module, &enum_.name, &enumerator.name);
return;
}
if let Some((enum_, enumerator)) = self.view.find_global_enum_constant(godot_const_ref)
{
let module = special_cases::get_global_enum_module_path(&enum_.godot_name);
write_enum_const_link(out, module, &enum_.name, &enumerator.name);
return;
}
}
write_code_span(out, godot_const_ref);
}
fn find_class_enum_constant(
&self,
class_godot_name: &str,
const_godot_name: &str,
) -> Option<(&'d Class, &'d Enum, &'d Enumerator)> {
if class_godot_name.starts_with('@') {
return None;
}
let class_ty = TyName::from_godot(class_godot_name);
let class = self.view.find_engine_class(&class_ty)?;
let (enum_, enumerator) = self
.view
.find_class_enum_constant(&class_ty, const_godot_name)?;
Some((class, enum_, enumerator))
}
fn find_class_enum_constant_in_hierarchy(
&self,
starting_class: &'d Class,
const_godot_name: &str,
) -> Option<(&'d Class, &'d Enum, &'d Enumerator)> {
let mut current = starting_class;
loop {
if let Some((enum_, enumerator)) = self
.view
.find_class_enum_constant(current.name(), const_godot_name)
{
return Some((current, enum_, enumerator));
}
let base_name = current.base_class.as_ref()?;
current = self.view.find_engine_class(base_name)?;
}
}
fn cached_class_rust_path(&mut self, godot_class_name: &str) -> &str {
if !self.path_cache.contains_key(godot_class_name) {
let path = get_class_rust_path(godot_class_name, self.ctx).into_owned();
self.path_cache.insert(godot_class_name.to_owned(), path);
}
&self.path_cache[godot_class_name]
}
fn remaining(&self) -> &'d str {
&self.doc[self.pos..]
}
}
fn write_code_span(out: &mut String, text: &str) {
write_str!(out, "`{text}`");
}
fn write_code_link(out: &mut String, label: &str, path: &str) {
write_str!(out, "[`{label}`][{path}]");
}
fn write_enum_const_link(
out: &mut String,
module_path: &str,
enum_name: &dyn std::fmt::Display,
variant: &dyn std::fmt::Display,
) {
write_str!(
out,
"[`{enum_name}::{variant}`][`{module_path}::{enum_name}::{variant}`]"
);
}
fn is_ident_like(str: &str) -> bool {
!str.is_empty()
&& str
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
fn is_type_link(str: &str) -> bool {
let mut chars = str.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphanumeric() && first != '@' {
return false;
}
chars.all(|ch| ch.is_ascii_alphanumeric())
}
fn starts_with_known_tag(str: &str) -> bool {
WRAPPED_TAGS.iter().any(|t| str.starts_with(t.open()))
|| str.starts_with("[url=")
|| str.starts_with("[codeblocks]")
|| str.starts_with("[codeblock lang=")
}
fn is_escaped_role(str: &str) -> bool {
let role = str.split_once(' ').map(|(r, _)| r).unwrap_or(str);
matches!(
role,
"constant" | "member" | "enum" | "signal" | "annotation" | "constructor"
)
}
fn convert_to_method_path(
class_method: &str,
surrounding_class: Option<&Class>,
ctx: &Context,
view: &ApiView,
path_cache: &mut HashMap<String, String>,
) -> Option<CowStr> {
let (link_godot_class, link_godot_method) =
if let Some((class_name, method_name)) = class_method.split_once('.') {
(class_name, method_name)
} else if let Some(class) = surrounding_class {
(class.name().godot_ty.as_str(), class_method)
} else {
return None;
};
let link_godot_method = util::safe_ident(link_godot_method).to_string();
match matches_hardcoded_method(link_godot_class, &link_godot_method) {
Hardcoded::Mapped(path) => return Some(path),
Hardcoded::Suppressed => return None,
Hardcoded::NotMatched => {}
}
if !ctx.is_builtin(link_godot_class) && special_cases::is_class_deleted_str(link_godot_class) {
return None;
}
let rust_class_path: &str = {
if !path_cache.contains_key(link_godot_class) {
let path = get_class_rust_path(link_godot_class, ctx).into_owned();
path_cache.insert(link_godot_class.to_owned(), path);
}
&path_cache[link_godot_class]
};
let Some(class) = view.find_engine_class(&TyName::from_godot(link_godot_class)) else {
return Some(format!("{rust_class_path}::{link_godot_method}").into());
};
let Some(method) = class
.methods
.iter()
.find(|method| method.godot_name() == link_godot_method)
else {
return None;
};
if special_cases::is_class_method_replaced_with_type_safe(class.name(), &link_godot_method) {
return Some(format!("{rust_class_path}::{link_godot_method}").into());
}
if method.is_private_in_final_api() {
return None;
}
if method.is_virtual() {
return (!class.is_final).then(|| {
format!(
"crate::classes::{}::{}",
class.name().virtual_trait_name(),
method.name()
)
.into()
});
}
Some(format!("{rust_class_path}::{}", method.name()).into())
}
fn matches_hardcoded_type(godot_class: &str) -> Option<&'static str> {
match godot_class {
"@GlobalScope" => Some("[@GlobalScope][crate::global]"),
_ => None,
}
}
enum Hardcoded {
Mapped(CowStr),
Suppressed,
NotMatched,
}
fn matches_hardcoded_method(godot_class: &str, godot_method: &str) -> Hardcoded {
let path: CowStr = match (godot_class, godot_method) {
("Object", "free") => "crate::obj::Gd::free".into(),
("Object", "get_instance_id") => "crate::obj::Gd::instance_id".into(),
("Object", "notification") => "crate::classes::Object::notify".into(),
("Object", "_notification") => "crate::classes::IObject::on_notification".into(),
("Object", "_init") => "crate::classes::IObject::init".into(),
("Object", "_validate_property") => "crate::classes::IObject::on_validate_property".into(),
("Object", "_get_property_list") => "crate::classes::IObject::on_get_property_list".into(),
("Object", "_get") => "crate::classes::IObject::on_get".into(),
("Object", "_set") => "crate::classes::IObject::on_set".into(),
("GDScript", "new") => "crate::obj::NewGd::new_gd".into(),
("String", "length") => "crate::builtin::GString::len".into(),
("String", "match_") => "crate::builtin::GString::match_glob".into(),
("Dictionary", "size") => "crate::builtin::Dictionary::len".into(),
("Array", "size") => "crate::builtin::AnyArray::len".into(),
("PackedByteArray", "size") => "crate::builtin::PackedByteArray::len".into(),
("Vector2", "min") => "crate::builtin::Vector2::coord_min".into(),
("Vector2", "max") => "crate::builtin::Vector2::coord_max".into(),
("Vector3", "min") => "crate::builtin::Vector3::coord_min".into(),
("Vector3", "max") => "crate::builtin::Vector3::coord_max".into(),
("Vector4", "min") => "crate::builtin::Vector4::coord_min".into(),
("Vector4", "max") => "crate::builtin::Vector4::coord_max".into(),
("Transform2D", "get_scale") => "crate::builtin::Transform2D::scale".into(),
("Node", "get_node") => "crate::classes::Node::get_node_as".into(),
("Color", "is_equal_approx") => "crate::builtin::math::ApproxEq::approx_eq".into(),
("@GlobalScope", "instance_from_id") => "crate::obj::Gd::from_instance_id".into(),
("@GlobalScope", "is_instance_valid") => "crate::obj::Gd::is_instance_valid".into(),
("@GDScript", "load") => "crate::tools::load".into(),
("@GDScript", "save") => "crate::tools::save".into(),
("@GlobalScope", _) => format!("crate::global::{godot_method}").into(),
("@GDScript", _) => return Hardcoded::Suppressed,
_ => return Hardcoded::NotMatched,
};
Hardcoded::Mapped(path)
}
fn convert_builtin_types(type_name: &str) -> Option<&'static str> {
match type_name {
"String" => Some("crate::builtin::GString"),
"Array" => Some("crate::builtin::Array"),
"Dictionary" => Some("crate::builtin::Dictionary"),
_ => None,
}
}
fn get_class_rust_path(godot_class_name: &str, ctx: &Context) -> CowStr {
if let Some(hardcoded_builtin_type) = convert_builtin_types(godot_class_name) {
return hardcoded_builtin_type.into();
}
let is_builtin = ctx.is_builtin(godot_class_name);
let rust_class_name = conv::to_pascal_case(godot_class_name);
if is_builtin {
format!("crate::builtin::{rust_class_name}").into()
} else {
format!("crate::classes::{rust_class_name}").into()
}
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
#[allow(non_snake_case)]
mod tests {
use std::cell::OnceCell;
use super::*;
use crate::models::api_json::load_extension_api;
use crate::models::domain::ExtensionApi;
struct DocTestCache {
ctx: Context,
api: ExtensionApi,
}
thread_local! {
static CACHE: OnceCell<DocTestCache> = const { OnceCell::new() };
}
fn import_doc_for_test(description: &str, surrounding_class_name: Option<&str>) -> String {
CACHE.with(|cell| {
let cache = cell.get_or_init(|| {
let mut watch = godot_bindings::StopWatch::start();
let json = load_extension_api(&mut watch);
let mut ctx = Context::build_from_api(&json);
let api = ExtensionApi::from_json(json, &mut ctx);
DocTestCache { ctx, api }
});
let view = ApiView::new(&cache.api);
let surrounding_class = surrounding_class_name
.and_then(|name| view.find_engine_class(&TyName::from_godot(name)));
import_docs(description, surrounding_class, &cache.ctx, &view)
})
}
#[test]
fn type__engine_classes() {
let description = "Inherits from [Node] or [Resource], depending on use case.";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"Inherits from [`Node`][crate::classes::Node] or [`Resource`][crate::classes::Resource], depending on use case."
);
}
#[test]
fn type__builtin_and_member_role() {
let description = "Link [member Vector2.x] and [member Vector2.y] on [Vector2] or \
[Vector3]. Use [code]\"suffix:px/s\"[/code] for the editor unit suffix.";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"Link \\[member Vector2.x] and \\[member Vector2.y] on \
[`Vector2`][crate::builtin::Vector2] or [`Vector3`][crate::builtin::Vector3]. Use \
`\"suffix:px/s\"` for the editor unit suffix."
);
}
#[test]
fn type__preserves_markdown_link() {
let description = "See [reference](https://example.com) and [Node].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"See [reference](https://example.com) and [`Node`][crate::classes::Node]."
);
}
#[test]
fn type__global_scope() {
let description = "Use [@GlobalScope].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Use [@GlobalScope][crate::global].");
}
#[test]
fn type__gdscript_plain_text() {
let description = "See [@GDScript].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "See @GDScript.");
}
#[test]
#[cfg(not(target_os = "android"))] #[cfg_attr(published_docs, doc(cfg(not(target_os = "android"))))]
fn type__deleted_class_backtick() {
let description = "See [JavaClass].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "See `JavaClass`.");
}
#[test]
fn type__primitive_links() {
let description = "Use [int], [float], and [bool].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Use `int`, `float`, and `bool`.");
}
#[test]
fn type__surrounding_class() {
let description = "See [Node] and [Object].";
let actual = import_doc_for_test(description, Some("Node"));
assert_eq!(actual, "See `Node` and [`Object`][crate::classes::Object].");
}
#[test]
fn method__with_newlines_and_roles() {
let description = "Compare [code]LEFT[/code] and [code]RIGHT[/code] variants.\n\
Use [method InputEvent.is_match] with [constant KEY_LOCATION_UNSPECIFIED] or [enum KeyLocation].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"Compare `LEFT` and `RIGHT` variants.\n\n\
Use [`is_match`][`crate::classes::InputEvent::is_match`] with \
[`KeyLocation::UNSPECIFIED`][`crate::global::KeyLocation::UNSPECIFIED`] or \\[enum KeyLocation]."
);
}
#[test]
fn method__in_surrounding_class() {
let description = "Call [method get_node] to fetch a child.";
let actual = import_doc_for_test(description, Some("Node"));
assert_eq!(
actual,
"Call [`get_node_as`][`crate::classes::Node::get_node_as`] to fetch a child."
);
}
#[test]
fn method__type_safe_replacement() {
let description = "See [method Object.get_script].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"See [`get_script`][`crate::classes::Object::get_script`]."
);
}
#[test]
fn constant__code_fallback() {
let description = "See [constant CONNECT_DEFERRED].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "See `CONNECT_DEFERRED`.");
}
#[test]
fn constant__notification_link_no_class() {
let description = "See [constant NOTIFICATION_ENTER_TREE].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"See [`NodeNotification::ENTER_TREE`][`crate::classes::notify::NodeNotification::ENTER_TREE`]."
);
}
#[test]
fn constant__notification_link_with_class() {
let description = "See [constant NOTIFICATION_READY].";
let actual = import_doc_for_test(description, Some("Node"));
assert_eq!(
actual,
"See [`NodeNotification::READY`][`crate::classes::notify::NodeNotification::READY`]."
);
}
#[test]
fn constant__notification_link_inherited() {
let description = "See [constant NOTIFICATION_POSTINITIALIZE].";
let actual = import_doc_for_test(description, Some("Node"));
assert_eq!(
actual,
"See [`NodeNotification::POSTINITIALIZE`][`crate::classes::notify::NodeNotification::POSTINITIALIZE`]."
);
}
#[test]
fn constant__class_enum_via_hierarchy() {
let description = "See [constant CONNECT_DEFERRED].";
let actual = import_doc_for_test(description, Some("Node"));
assert_eq!(
actual,
"See [`ConnectFlags::DEFERRED`][`crate::classes::object::ConnectFlags::DEFERRED`]."
);
}
#[test]
fn constant__dot_class_enum_link() {
let description = "See [constant Object.CONNECT_DEFERRED].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"See [`ConnectFlags::DEFERRED`][`crate::classes::object::ConnectFlags::DEFERRED`]."
);
}
#[test]
fn constant__dot_notification_link() {
let description = "See [constant Node.NOTIFICATION_READY].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"See [`NodeNotification::READY`][`crate::classes::notify::NodeNotification::READY`]."
);
}
#[test]
fn constant__global_enum_link() {
let description = "See [constant MIDI_MESSAGE_NOTE_ON].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"See [`MidiMessage::NOTE_ON`][`crate::global::MidiMessage::NOTE_ON`]."
);
}
#[test]
fn code_block__preserves_contents() {
let description = "Bit mask used to remove modifiers before checking a keycode.\n\
[codeblock]\n\
var keycode = KEY_A | KEY_MASK_SHIFT\n\
keycode = keycode & KEY_CODE_MASK\n\
[/codeblock]";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"Bit mask used to remove modifiers before checking a keycode.\n\n\
```gdscript\n\
var keycode = KEY_A | KEY_MASK_SHIFT\n\
keycode = keycode & KEY_CODE_MASK\n\
```"
);
}
#[test]
fn code_block__type_literal() {
let description = "[codeblock]\n[Node]\n[/codeblock]";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "```gdscript\n[Node]\n```");
}
#[test]
fn code_block__other_variants() {
let codeblocks_description = "[codeblocks]alpha\nbeta[/codeblocks]";
let codeblock_lang_description = "[codeblock lang=text]\nalpha\nbeta\n[/codeblock]";
let gdscript_description = "[gdscript]\nprint(\"hi\")\n[/gdscript]";
let csharp_description = "[csharp]\nGD.Print(\"hi\");\n[/csharp]";
let codeblocks_actual = import_doc_for_test(codeblocks_description, None);
let codeblock_lang_actual = import_doc_for_test(codeblock_lang_description, None);
let gdscript_actual = import_doc_for_test(gdscript_description, None);
let csharp_actual = import_doc_for_test(csharp_description, None);
assert_eq!(codeblocks_actual, "alpha\nbeta");
assert_eq!(codeblock_lang_actual, "```text\nalpha\nbeta\n```");
assert_eq!(gdscript_actual, "```gdscript\nprint(\"hi\")\n```");
assert_eq!(csharp_actual, ""); }
#[test]
fn codeblocks__nested_languages() {
let description = "[codeblocks]\n\
[gdscript]\n\
print(\"hi\")\n\
[/gdscript]\n\
[csharp]\n\
GD.Print(\"hi\");\n\
[/csharp]\n\
[/codeblocks]";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"\n\
```gdscript\n\
print(\"hi\")\n\
```\n" );
}
#[test]
fn code__skip_lint() {
let description =
"Use [code skip-lint]x[/code] and [code skip-lint][url=address]text[/url][/code].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Use `x` and `[url=address]text[/url]`.");
}
#[test]
fn code__member_literal() {
let description = "Literal [code][member Vector2.x][/code].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Literal `[member Vector2.x]`.");
}
#[test]
fn code__type_literal() {
let description = "Literal [code][Node][/code].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Literal `[Node]`.");
}
#[test]
fn code__method_literal() {
let description = "Literal [code][method lerp][/code].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Literal `[method lerp]`.");
}
#[test]
fn bold__with_code_and_member_role() {
let description = "MIDI note release.\n\
[b]Note:[/b] Some devices send [constant MIDI_MESSAGE_NOTE_ON] with [member InputEventMIDI.velocity] = [code]0[/code].";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"MIDI note release.\n\n\
**Note:** Some devices send [`MidiMessage::NOTE_ON`][`crate::global::MidiMessage::NOTE_ON`] with \
\\[member InputEventMIDI.velocity] = `0`."
);
}
#[test]
fn url__basic() {
let description = "Controller docs vary; see \
[url=https://example.com/spec]the spec[/url] for sliders, pedals, and similar inputs.";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"Controller docs vary; see [the spec](https://example.com/spec) for sliders, \
pedals, and similar inputs."
);
}
#[test]
fn italic__basic() {
let description = "The current instrument is often called [i]program[/i] or \
[i]preset[/i] in MIDI docs.";
let actual = import_doc_for_test(description, None);
assert_eq!(
actual,
"The current instrument is often called _program_ or _preset_ in MIDI docs."
);
}
#[test]
fn param__nested_in_bold() {
let description = "[b]Use [param count][/b].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "**Use `count`**.");
}
#[test]
fn kbd__basic() {
let description = "Press [kbd]Ctrl + S[/kbd].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Press `Ctrl + S`.");
}
#[test]
fn signal__code_fallback() {
let description = "Emit [signal pressed].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Emit `pressed`.");
}
#[test]
fn annotation__code_fallback() {
let description = "Use [annotation @GDScript.@rpc].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Use `@GDScript.@rpc`.");
}
#[test]
fn constructor__code_fallback() {
let description = "Create [constructor Transform2D].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Create `Transform2D()`.");
}
#[test]
fn type__followed_by_http_url() {
let description = "See [Node](https://example.com) for details.";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "See [Node](https://example.com) for details.");
}
#[test]
fn type__followed_by_plural_suffix() {
let description = "Use [Node](s).";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Use [`Node`][crate::classes::Node](s).");
}
#[test]
fn bbcode__unclosed_tag_literal() {
let description = "[b]hello";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "[b]hello");
}
#[test]
fn code__nested_bold() {
let description = "Use [code][b]x[/b][/code].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Use `[b]x[/b]`.");
}
#[test]
fn empty_brackets() {
let description = "Edge case: [].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Edge case: [].");
}
#[test]
fn param__non_ident_left_literal() {
let description = "Bad name [param foo-bar].";
let actual = import_doc_for_test(description, None);
assert_eq!(actual, "Bad name [param foo-bar].");
}
}