1#![allow(clippy::manual_map)]
2use std::{fmt::Write, sync::OnceLock};
3
4use log::{info, warn};
5use regex::{Captures, Regex};
6
7use super::{gi_docgen, LocationInObject};
8use crate::{
9 analysis::functions::Info,
10 library::{FunctionKind, TypeId},
11 nameutil, Env,
12};
13
14const LANGUAGE_SEP_BEGIN: &str = "<!-- language=\"";
15const LANGUAGE_SEP_END: &str = "\" -->";
16const LANGUAGE_BLOCK_BEGIN: &str = "|[";
17const LANGUAGE_BLOCK_END: &str = "\n]|";
18
19const IGNORE_C_WARNING_FUNCS: [&str; 6] = [
22 "g_object_unref",
23 "g_object_ref",
24 "g_free",
25 "g_list_free",
26 "g_strfreev",
27 "printf",
28];
29
30pub fn reformat_doc(
31 input: &str,
32 env: &Env,
33 in_type: Option<(&TypeId, Option<LocationInObject>)>,
34) -> String {
35 code_blocks_transformation(input, env, in_type)
36}
37
38fn try_split<'a>(src: &'a str, needle: &str) -> (&'a str, Option<&'a str>) {
39 match src.find(needle) {
40 Some(pos) => (&src[..pos], Some(&src[pos + needle.len()..])),
41 None => (src, None),
42 }
43}
44
45fn code_blocks_transformation(
46 mut input: &str,
47 env: &Env,
48 in_type: Option<(&TypeId, Option<LocationInObject>)>,
49) -> String {
50 let mut out = String::with_capacity(input.len());
51
52 loop {
53 input = match try_split(input, LANGUAGE_BLOCK_BEGIN) {
54 (before, Some(after)) => {
55 out.push_str(&format(before, env, in_type));
56 if let (before, Some(after)) =
57 try_split(get_language(after, &mut out), LANGUAGE_BLOCK_END)
58 {
59 out.push_str(before);
60 out.push_str("\n```");
61 after
62 } else {
63 after
64 }
65 }
66 (before, None) => {
67 out.push_str(&format(before, env, in_type));
68 return out;
69 }
70 };
71 }
72}
73
74fn get_language<'a>(entry: &'a str, out: &mut String) -> &'a str {
75 if let (_, Some(after)) = try_split(entry, LANGUAGE_SEP_BEGIN) {
76 if let (before, Some(after)) = try_split(after, LANGUAGE_SEP_END) {
77 if !["text", "rust"].contains(&before) {
78 write!(out, "\n\n**⚠️ The following code is in {before} ⚠️**").unwrap();
79 }
80 write!(out, "\n\n```{before}").unwrap();
81 return after;
82 }
83 }
84 out.push_str("\n```text");
85 entry
86}
87
88fn get_markdown_language(input: &str) -> (&str, &str) {
90 let (lang, after) = if let Some((lang, after)) = input.split_once('\n') {
91 let lang = if lang.is_empty() { None } else { Some(lang) };
92 (lang, after)
93 } else {
94 (None, input)
95 };
96 (lang.unwrap_or("text"), after)
97}
98
99fn format(
101 mut input: &str,
102 env: &Env,
103 in_type: Option<(&TypeId, Option<LocationInObject>)>,
104) -> String {
105 let mut ret = String::with_capacity(input.len());
106 loop {
107 input = match try_split(input, "```") {
108 (before, Some(after)) => {
109 ret.push_str(&replace_symbols(before, env, in_type));
111
112 let (lang, after) = get_markdown_language(after);
113 if !["text", "rust", "xml", "css", "json", "html"].contains(&lang)
114 && after.lines().count() > 1
115 {
116 write!(ret, "**⚠️ The following code is in {lang} ⚠️**\n\n").unwrap();
117 }
118 writeln!(ret, "```{lang}").unwrap();
119
120 if let (before, Some(after)) = try_split(after, "```") {
121 ret.push_str(before);
122 ret.push_str("```");
123 after
124 } else {
125 after
126 }
127 }
128 (before, None) => {
129 ret.push_str(&replace_symbols(before, env, in_type));
130 return ret;
131 }
132 }
133 }
134}
135
136fn replace_symbols(
137 input: &str,
138 env: &Env,
139 in_type: Option<(&TypeId, Option<LocationInObject>)>,
140) -> String {
141 if env.config.use_gi_docgen {
142 let out = gi_docgen::replace_c_types(input, env, in_type);
143 let out = gi_docgen_symbol().replace_all(&out, |caps: &Captures<'_>| match &caps[2] {
144 "TRUE" => "[`true`]".to_string(),
145 "FALSE" => "[`false`]".to_string(),
146 "NULL" => "[`None`]".to_string(),
147 symbol_name => match &caps[1] {
148 "%" => find_constant_or_variant_wrapper(symbol_name, env, in_type),
150 s => panic!("Unknown symbol prefix `{s}`"),
151 },
152 });
153 let out = gdk_gtk().replace_all(&out, |caps: &Captures<'_>| {
154 find_type(&caps[2], env).unwrap_or_else(|| format!("`{}`", &caps[2]))
155 });
156
157 out.to_string()
158 } else {
159 replace_c_types(input, env, in_type)
160 }
161}
162
163fn symbol() -> &'static Regex {
164 static REGEX: OnceLock<Regex> = OnceLock::new();
165 REGEX.get_or_init(|| Regex::new(r"([@#%])(\w+\b)([:.]+[\w-]+\b)?").unwrap())
166}
167
168fn gi_docgen_symbol() -> &'static Regex {
169 static REGEX: OnceLock<Regex> = OnceLock::new();
170 REGEX.get_or_init(|| Regex::new(r"([%])(\w+\b)([:.]+[\w-]+\b)?").unwrap())
171}
172
173fn function() -> &'static Regex {
174 static REGEX: OnceLock<Regex> = OnceLock::new();
175 REGEX.get_or_init(|| Regex::new(r"([@#%])?(\w+\b[:.]+)?(\b[a-z0-9_]+)\(\)").unwrap())
176}
177
178fn gdk_gtk() -> &'static Regex {
179 static REGEX: OnceLock<Regex> = OnceLock::new();
184 REGEX.get_or_init(|| {
185 Regex::new(r"`([^\(:])?((G[dts]k|Pango|cairo_|graphene_|Adw|Hdy|GtkSource)\w+\b)(\.)?`")
186 .unwrap()
187 })
188}
189
190fn tags() -> &'static Regex {
191 static REGEX: OnceLock<Regex> = OnceLock::new();
192 REGEX.get_or_init(|| Regex::new(r"<[\w/-]+>").unwrap())
193}
194
195fn spaces() -> &'static Regex {
196 static REGEX: OnceLock<Regex> = OnceLock::new();
197 REGEX.get_or_init(|| Regex::new(r"[ ]{2,}").unwrap())
198}
199
200fn replace_c_types(
201 entry: &str,
202 env: &Env,
203 in_type: Option<(&TypeId, Option<LocationInObject>)>,
204) -> String {
205 let out = function().replace_all(entry, |caps: &Captures<'_>| {
206 let name = &caps[3];
207 find_method_or_function_by_ctype(None, name, env, in_type).unwrap_or_else(|| {
208 if !IGNORE_C_WARNING_FUNCS.contains(&name) {
209 info!("No function found for `{}()`", name);
210 }
211 format!("`{}{}()`", caps.get(2).map_or("", |m| m.as_str()), name)
212 })
213 });
214
215 let out = symbol().replace_all(&out, |caps: &Captures<'_>| match &caps[2] {
216 "TRUE" => "[`true`]".to_string(),
217 "FALSE" => "[`false`]".to_string(),
218 "NULL" => "[`None`]".to_string(),
219 symbol_name => match &caps[1] {
220 "%" => find_constant_or_variant_wrapper(symbol_name, env, in_type),
221 "#" => {
222 if let Some(member_path) = caps.get(3).map(|m| m.as_str()) {
223 let method_name = member_path.trim_start_matches('.');
224 find_member(symbol_name, method_name, env, in_type).unwrap_or_else(|| {
225 info!("`#{}` not found as method", symbol_name);
226 format!("`{symbol_name}{member_path}`")
227 })
228 } else if let Some(type_) = find_type(symbol_name, env) {
229 type_
230 } else if let Some(constant_or_variant) =
231 find_constant_or_variant(symbol_name, env, in_type)
232 {
233 warn!(
234 "`{}` matches a constant/variant and should use `%` prefix instead of `#`",
235 symbol_name
236 );
237 constant_or_variant
238 } else {
239 info!("Type `#{}` not found", symbol_name);
240 format!("`{symbol_name}`")
241 }
242 }
243 "@" => {
244 if let Some(type_) = find_type(symbol_name, env) {
247 warn!(
248 "`{}` matches a type and should use `#` prefix instead of `%`",
249 symbol_name
250 );
251 type_
252 } else if let Some(constant_or_variant) =
253 find_constant_or_variant(symbol_name, env, in_type)
254 {
255 constant_or_variant
256 } else if let Some(function) =
257 find_method_or_function_by_ctype(None, symbol_name, env, in_type)
258 {
259 function
260 } else {
261 format!("`{symbol_name}`")
263 }
264 }
265 s => panic!("Unknown symbol prefix `{s}`"),
266 },
267 });
268 let out = gdk_gtk().replace_all(&out, |caps: &Captures<'_>| {
269 find_type(&caps[2], env).unwrap_or_else(|| format!("`{}`", &caps[2]))
270 });
271 let out = tags().replace_all(&out, "`$0`");
272 spaces().replace_all(&out, " ").into_owned()
273}
274
275fn find_constant_or_variant_wrapper(
278 symbol_name: &str,
279 env: &Env,
280 in_type: Option<(&TypeId, Option<LocationInObject>)>,
281) -> String {
282 find_constant_or_variant(symbol_name, env, in_type).unwrap_or_else(|| {
283 info!("Constant or variant `%{}` not found", symbol_name);
284 format!("`{symbol_name}`")
285 })
286}
287
288fn find_member(
289 type_: &str,
290 method_name: &str,
291 env: &Env,
292 in_type: Option<(&TypeId, Option<LocationInObject>)>,
293) -> Option<String> {
294 let symbols = env.symbols.borrow();
295 let is_signal = method_name.starts_with("::");
296 let is_property = !is_signal && method_name.starts_with(':');
297 if !is_signal && !is_property {
298 find_method_or_function_by_ctype(Some(type_), method_name, env, in_type)
299 } else {
300 env.analysis
301 .objects
302 .values()
303 .find(|o| o.c_type == type_)
304 .map(|info| {
305 let sym = symbols.by_tid(info.type_id).unwrap(); let name = method_name.trim_start_matches(':');
307 if is_property {
308 gen_property_doc_link(&sym.full_rust_name(), name)
309 } else {
310 gen_signal_doc_link(&sym.full_rust_name(), name)
311 }
312 })
313 }
314}
315
316fn find_constant_or_variant(
317 symbol: &str,
318 env: &Env,
319 in_type: Option<(&TypeId, Option<LocationInObject>)>,
320) -> Option<String> {
321 if let Some((flag_info, member_info)) = env.analysis.flags.iter().find_map(|f| {
322 f.type_(&env.library)
323 .members
324 .iter()
325 .find(|m| m.c_identifier == symbol && !m.status.ignored())
326 .map(|m| (f, m))
327 }) {
328 Some(gen_member_doc_link(
329 flag_info.type_id,
330 &nameutil::bitfield_member_name(&member_info.name),
331 env,
332 in_type,
333 ))
334 } else if let Some((enum_info, member_info)) = env.analysis.enumerations.iter().find_map(|e| {
335 e.type_(&env.library)
336 .members
337 .iter()
338 .find(|m| m.c_identifier == symbol && !m.status.ignored())
339 .map(|m| (e, m))
340 }) {
341 Some(gen_member_doc_link(
342 enum_info.type_id,
343 &nameutil::enum_member_name(&member_info.name),
344 env,
345 in_type,
346 ))
347 } else if let Some(const_info) = env
348 .analysis
349 .constants
350 .iter()
351 .find(|c| c.glib_name == symbol)
352 {
353 Some(gen_const_doc_link(const_info))
354 } else {
355 None
356 }
357}
358
359const IGNORED_C_TYPES: [&str; 6] = [
361 "gconstpointer",
362 "guint16",
363 "guint",
364 "gunicode",
365 "gchararray",
366 "GList",
367];
368fn find_type(type_: &str, env: &Env) -> Option<String> {
370 if IGNORED_C_TYPES.contains(&type_) {
371 return None;
372 }
373
374 let type_id = if let Some(obj) = env.analysis.objects.values().find(|o| o.c_type == type_) {
375 Some(obj.type_id)
376 } else if let Some(record) = env
377 .analysis
378 .records
379 .values()
380 .find(|r| r.type_(&env.library).c_type == type_)
381 {
382 Some(record.type_id)
383 } else if let Some(enum_) = env
384 .analysis
385 .enumerations
386 .iter()
387 .find(|e| e.type_(&env.library).c_type == type_)
388 {
389 Some(enum_.type_id)
390 } else if let Some(flag) = env
391 .analysis
392 .flags
393 .iter()
394 .find(|f| f.type_(&env.library).c_type == type_)
395 {
396 Some(flag.type_id)
397 } else {
398 None
399 };
400
401 type_id.map(|ty| gen_symbol_doc_link(ty, env))
402}
403
404fn find_method_or_function_by_ctype(
405 c_type: Option<&str>,
406 name: &str,
407 env: &Env,
408 in_type: Option<(&TypeId, Option<LocationInObject>)>,
409) -> Option<String> {
410 find_method_or_function(
411 env,
412 in_type,
413 |f| f.glib_name == name,
414 |o| c_type.is_none_or(|t| o.c_type == t),
415 |r| c_type.is_none_or(|t| r.type_(&env.library).c_type == t),
416 |r| c_type.is_none_or(|t| r.type_(&env.library).c_type == t),
417 |r| c_type.is_none_or(|t| r.type_(&env.library).c_type == t),
418 c_type.is_some_and(|t| t.ends_with("Class")),
419 false,
420 )
421}
422
423pub(crate) fn find_method_or_function(
434 env: &Env,
435 in_type: Option<(&TypeId, Option<LocationInObject>)>,
436 search_fn: impl Fn(&crate::analysis::functions::Info) -> bool + Copy,
437 search_obj: impl Fn(&crate::analysis::object::Info) -> bool + Copy,
438 search_record: impl Fn(&crate::analysis::record::Info) -> bool + Copy,
439 search_enum: impl Fn(&crate::analysis::enums::Info) -> bool + Copy,
440 search_flag: impl Fn(&crate::analysis::flags::Info) -> bool + Copy,
441 is_class_method: bool,
442 is_virtual_method: bool,
443) -> Option<String> {
444 if is_virtual_method {
445 if let Some((obj_info, fn_info)) = env
446 .analysis
447 .find_object_by_virtual_method(env, search_obj, search_fn)
448 {
449 Some(gen_object_fn_doc_link(
450 obj_info,
451 fn_info,
452 env,
453 in_type,
454 &obj_info.name,
455 ))
456 } else {
457 None
458 }
459 } else if is_class_method {
460 if let Some((record_info, fn_info)) =
461 env.analysis
462 .find_record_by_function(env, search_record, search_fn)
463 {
464 let object = env.config.objects.get(&record_info.full_name);
465 let visible_parent = object
466 .and_then(|o| o.trait_name.clone())
467 .unwrap_or_else(|| format!("{}Ext", record_info.name));
468 let parent = format!("subclass::prelude::{}", visible_parent);
469 let is_self = in_type == Some((&record_info.type_id, None));
470 Some(fn_info.doc_link(Some(&parent), Some(&visible_parent), is_self))
471 } else {
472 None
473 }
474 } else if let Some((obj_info, fn_info)) = env
476 .analysis
477 .find_object_by_function(env, search_obj, search_fn)
478 {
479 Some(gen_object_fn_doc_link(
480 obj_info,
481 fn_info,
482 env,
483 in_type,
484 &obj_info.name,
485 ))
486 } else if let Some((record_info, fn_info)) =
488 env.analysis
489 .find_record_by_function(env, search_record, search_fn)
490 {
491 Some(gen_type_fn_doc_link(
492 record_info.type_id,
493 fn_info,
494 env,
495 in_type,
496 ))
497 } else if let Some((enum_info, fn_info)) =
498 env.analysis
499 .find_enum_by_function(env, search_enum, search_fn)
500 {
501 Some(gen_type_fn_doc_link(
502 enum_info.type_id,
503 fn_info,
504 env,
505 in_type,
506 ))
507 } else if let Some((flag_info, fn_info)) =
508 env.analysis
509 .find_flag_by_function(env, search_flag, search_fn)
510 {
511 Some(gen_type_fn_doc_link(
512 flag_info.type_id,
513 fn_info,
514 env,
515 in_type,
516 ))
517 } else if let Some(fn_info) = env.analysis.find_global_function(env, search_fn) {
519 Some(fn_info.doc_link(None, None, false))
520 } else {
521 None
522 }
523}
524
525pub(crate) fn gen_type_fn_doc_link(
526 type_id: TypeId,
527 fn_info: &Info,
528 env: &Env,
529 in_type: Option<(&TypeId, Option<LocationInObject>)>,
530) -> String {
531 let symbols = env.symbols.borrow();
532 let sym_name = symbols.by_tid(type_id).unwrap().full_rust_name();
533 let is_self = in_type == Some((&type_id, None));
534
535 fn_info.doc_link(Some(&sym_name), None, is_self)
536}
537
538pub(crate) fn gen_object_fn_doc_link(
539 obj_info: &crate::analysis::object::Info,
540 fn_info: &Info,
541 env: &Env,
542 in_type: Option<(&TypeId, Option<LocationInObject>)>,
543 visible_name: &str,
544) -> String {
545 let symbols = env.symbols.borrow();
546 let sym = symbols.by_tid(obj_info.type_id).unwrap();
547 let is_self = in_type == Some((&obj_info.type_id, Some(obj_info.function_location(fn_info))));
548
549 if fn_info.kind == FunctionKind::VirtualMethod || fn_info.kind == FunctionKind::ClassMethod {
550 let (type_name, visible_type_name) = obj_info.generate_doc_link_info(fn_info);
551 fn_info.doc_link(
552 Some(&sym.full_rust_name().replace(visible_name, &type_name)),
553 Some(&visible_type_name),
554 false,
555 )
556 } else if fn_info.kind == FunctionKind::Method {
557 let (type_name, visible_type_name) = obj_info.generate_doc_link_info(fn_info);
558
559 fn_info.doc_link(
560 Some(&sym.full_rust_name().replace(visible_name, &type_name)),
561 Some(&visible_type_name),
562 is_self,
563 )
564 } else {
565 fn_info.doc_link(Some(&sym.full_rust_name()), None, is_self)
566 }
567}
568
569pub(crate) fn gen_member_doc_link(
571 type_id: TypeId,
572 member_name: &str,
573 env: &Env,
574 in_type: Option<(&TypeId, Option<LocationInObject>)>,
575) -> String {
576 let symbols = env.symbols.borrow();
577 let sym = symbols.by_tid(type_id).unwrap().full_rust_name();
578 let is_self = in_type == Some((&type_id, None));
579
580 if is_self {
581 format!("[`{member_name}`][Self::{member_name}]")
582 } else {
583 format!("[`{sym}::{member_name}`][crate::{sym}::{member_name}]")
584 }
585}
586
587pub(crate) fn gen_const_doc_link(const_info: &crate::analysis::constants::Info) -> String {
588 format!("[`{n}`][crate::{n}]", n = const_info.name)
590}
591
592pub(crate) fn gen_signal_doc_link(symbol: &str, signal: &str) -> String {
593 format!("[`{signal}`][struct@crate::{symbol}#{signal}]")
594}
595
596pub(crate) fn gen_property_doc_link(symbol: &str, property: &str) -> String {
597 format!("[`{property}`][struct@crate::{symbol}#{property}]")
598}
599
600pub(crate) fn gen_vfunc_doc_link(symbol: &str, vfunc: &str) -> String {
601 format!("`vfunc::{symbol}::{vfunc}`")
602}
603
604pub(crate) fn gen_callback_doc_link(callback: &str) -> String {
605 format!("`callback::{callback}")
606}
607
608pub(crate) fn gen_alias_doc_link(alias: &str) -> String {
609 format!("`alias::{alias}`")
610}
611
612pub(crate) fn gen_symbol_doc_link(type_id: TypeId, env: &Env) -> String {
613 let symbols = env.symbols.borrow();
614 let sym = symbols.by_tid(type_id).unwrap();
615 if sym.name() == "Variant" && (sym.crate_name().is_none() || sym.crate_name() == Some("glib")) {
617 format!("[`{n}`][struct@crate::{n}]", n = sym.full_rust_name())
618 } else {
619 format!("[`{n}`][crate::{n}]", n = sym.full_rust_name())
620 }
621}