deno_doc 0.196.0

doc generation for deno
Documentation
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

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)
  }
}

/// Inspects leading comments in the source and returns the first JSDoc comment
/// with a `@module` tag along with its associated range, otherwise returns
/// `None`.
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(),
  }
}

/// If the jsdoc has an `@internal` or `@ignore` tag.
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
/"#
    )
  }
}