use deno_ast::SourcePos;
use deno_ast::SourceRange;
use deno_ast::SourceRangedForSpanned;
use deno_ast::SourceTextInfo;
use deno_ast::swc::ast::ModuleExportName;
use deno_ast::swc::common::comments::Comment;
use deno_ast::swc::common::comments::CommentKind;
use deno_graph::symbols::EsModuleInfo;
use regex::Regex;
use crate::js_doc::JsDoc;
use crate::js_doc::JsDocTag;
use crate::node::Location;
lazy_static! {
static ref JS_DOC_RE: Regex = Regex::new(r"^\s*\* ?").unwrap();
}
pub(crate) fn is_false(b: &bool) -> bool {
!b
}
fn parse_js_doc(js_doc_comment: &Comment, module_info: &EsModuleInfo) -> JsDoc {
JsDoc::new(remove_stars_from_js_doc(&js_doc_comment.text), module_info)
}
fn remove_stars_from_js_doc(text: &str) -> String {
text
.split('\n')
.map(|line| JS_DOC_RE.replace(line, "").to_string())
.collect::<Vec<String>>()
.join("\n")
.trim()
.to_string()
}
pub(crate) fn js_doc_for_range_include_ignore(
module_info: &EsModuleInfo,
range: &SourceRange,
) -> JsDoc {
let Some(comments) = module_info.source().comments().get_leading(range.start)
else {
return JsDoc::default();
};
if let Some(js_doc_comment) = comments.iter().rev().find(|comment| {
comment.kind == CommentKind::Block && comment.text.starts_with('*')
}) {
parse_js_doc(js_doc_comment, module_info)
} else {
JsDoc::default()
}
}
pub(crate) fn js_doc_for_range(
module_info: &EsModuleInfo,
range: &SourceRange,
) -> Option<JsDoc> {
let js_doc = js_doc_for_range_include_ignore(module_info, range);
if js_doc.tags.contains(&JsDocTag::Ignore) {
None
} else {
Some(js_doc)
}
}
pub(crate) fn module_js_doc_for_source(
module_info: &EsModuleInfo,
) -> Option<Option<(JsDoc, SourceRange)>> {
module_info
.source()
.comments()
.get_vec()
.into_iter()
.filter(|comment| {
comment.kind == CommentKind::Block && comment.text.starts_with('*')
})
.find_map(|comment| {
let js_doc = parse_js_doc(&comment, module_info);
if js_doc
.tags
.iter()
.any(|tag| matches!(tag, JsDocTag::Module { .. }))
{
if js_doc.tags.contains(&JsDocTag::Ignore) {
Some(None)
} else {
Some(Some((js_doc, comment.range())))
}
} else {
None
}
})
}
pub fn get_location(module_info: &EsModuleInfo, pos: SourcePos) -> Location {
get_text_info_location(
module_info.specifier().as_str(),
module_info.source().text_info_lazy(),
pos,
)
}
pub fn get_text_info_location(
specifier: &str,
text_info: &SourceTextInfo,
pos: SourcePos,
) -> Location {
let line_and_column_index =
text_info.line_and_column_display_with_indent_width(pos, 2);
let byte_index = pos.as_byte_index(text_info.range().start);
Location {
filename: specifier.into(),
line: line_and_column_index.line_number - 1,
col: line_and_column_index.column_number - 1,
byte_index,
}
}
pub fn module_export_name_value(
module_export_name: &ModuleExportName,
) -> String {
match module_export_name {
ModuleExportName::Ident(ident) => ident.sym.to_string(),
ModuleExportName::Str(str) => str.value.to_string_lossy().into_owned(),
}
}
pub fn has_ignorable_js_doc_tag(js_doc: &JsDoc) -> bool {
js_doc
.tags
.iter()
.any(|t| *t == JsDocTag::Ignore || *t == JsDocTag::Internal)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remove_stars_from_js_doc_works() {
assert_eq!(
remove_stars_from_js_doc(
"/**
* This module provides the `Result` class
*/"
),
"/**
This module provides the `Result` class
/"
);
assert_eq!(
remove_stars_from_js_doc(
r#"/**
* # Program
*
* description
*
* ## Usage
*
* @example
* ```ts
* import * as mod from "program";
*/"#
),
r#"/**
# Program
description
## Usage
@example
```ts
import * as mod from "program";
/"#
);
assert_eq!(
remove_stars_from_js_doc(
r#"/**
* # Program
* **Example:**
*
* example1
*/
"#
),
r#"/**
# Program
**Example:**
example1
/"#
)
}
}