dmc-transform 0.3.0

Transform pipeline and built-in transformers for the dmc compiler
Documentation
use dmc_parser::ast::*;
use dmc_parser::parse;
use dmc_transform::{AssignHeadingIds, AutolinkHeadings, CodeImport, Pipeline};

fn anchor_href(node: &Node) -> Option<String> {
  match node {
    Node::JsxElement(e) if e.name == "a" => e.attrs.iter().find_map(|a| match (&a.name[..], &a.value) {
      ("href", JsxAttrValue::String(s)) => Some(s.clone()),
      _ => None,
    }),
    _ => None,
  }
}

#[test]
fn pipeline_runs_autolink() {
  let mut d = parse("# Hello");
  Pipeline::new().add(AssignHeadingIds::new()).add(AutolinkHeadings::new()).run_silent(&mut d);
  let h = match &d.children[0] {
    Node::Heading(h) => h,
    n => panic!("expected heading, got {:?}", n),
  };
  // Prepend mode: anchor is the FIRST child, original text follows.
  assert!(h.children.len() >= 2, "expected anchor + text, got {:?}", h.children);
  assert_eq!(anchor_href(&h.children[0]).as_deref(), Some("#hello"));
}

#[test]
fn idempotent() {
  let mut d = parse("# Hello");
  Pipeline::new().add(AssignHeadingIds::new()).add(AutolinkHeadings::new()).run_silent(&mut d);
  let first_pass_len = match &d.children[0] {
    Node::Heading(h) => h.children.len(),
    _ => unreachable!(),
  };
  Pipeline::new().add(AssignHeadingIds::new()).add(AutolinkHeadings::new()).run_silent(&mut d);
  let h = match &d.children[0] {
    Node::Heading(h) => h,
    _ => unreachable!(),
  };
  assert_eq!(h.children.len(), first_pass_len, "autolink should not double-prepend");
}

#[test]
fn defaults_pipeline_includes_autolink() {
  let mut d = parse("# Foo Bar");
  Pipeline::with_defaults().run_silent(&mut d);
  let h = match &d.children[0] {
    Node::Heading(h) => h,
    n => panic!("expected heading, got {:?}", n),
  };
  assert_eq!(anchor_href(h.children.first().unwrap()).as_deref(), Some("#foo-bar"));
}

#[test]
fn dedupe_assigns_unique_ids() {
  let mut d = parse("## Patch Changes\n\n## Patch Changes\n\n## Patch Changes\n");
  Pipeline::new().add(AssignHeadingIds::new()).run_silent(&mut d);
  let ids: Vec<_> = d
    .children
    .iter()
    .filter_map(|n| match n {
      Node::Heading(h) => h.id.clone(),
      _ => None,
    })
    .collect();
  assert_eq!(ids, vec!["patch-changes", "patch-changes-1", "patch-changes-2"]);
}

#[test]
fn slug_strips_dots() {
  let mut d = parse("## 0.4.3\n");
  Pipeline::new().add(AssignHeadingIds::new()).run_silent(&mut d);
  let h = match &d.children[0] {
    Node::Heading(h) => h,
    _ => unreachable!(),
  };
  assert_eq!(h.id.as_deref(), Some("043"));
}

// #[test]
// fn npm_command_derives_yarn_pnpm_bun() {
//   let mut d = dmc_parser::parse("```\nnpm install lodash\n```\n");
//   dmc_transform::Pipeline::new().add(NpmCommand).run_silent(&mut d);
//   let cb = d
//     .children
//     .iter()
//     .find_map(|n| match n {
//       Node::CodeBlock(cb) => Some(cb),
//       _ => None,
//     })
//     .expect("code block");
//   let c = cb.commands.as_ref().expect("commands");
//   assert_eq!(c.npm, "npm install lodash");
//   assert_eq!(c.yarn, "yarn add lodash");
//   assert_eq!(c.pnpm, "pnpm add lodash");
//   assert_eq!(c.bun, "bun add lodash");
// }
//
// #[test]
// fn npm_command_handles_npx_create() {
//   let mut d = dmc_parser::parse("```\nnpx create-next-app my-app\n```\n");
//   dmc_transform::Pipeline::new().add(NpmCommand).run_silent(&mut d);
//   let cb = d
//     .children
//     .iter()
//     .find_map(|n| match n {
//       Node::CodeBlock(cb) => Some(cb),
//       _ => None,
//     })
//     .expect("cb");
//   let c = cb.commands.as_ref().expect("c");
//   assert_eq!(c.bun, "bunx create-next-app my-app");
// }

#[test]
fn code_import_reads_file() {
  let dir = tempfile::tempdir().unwrap();
  let snippet = dir.path().join("snippet.ts");
  std::fs::write(&snippet, "export const x = 1\n").unwrap();
  let src = "```ts file=\"snippet.ts\"\nplaceholder\n```\n".to_string();
  let mut d = dmc_parser::parse(&src);
  dmc_transform::Pipeline::new().add(CodeImport::with_base_dir(dir.path().to_path_buf())).run_silent(&mut d);
  let cb = d
    .children
    .iter()
    .find_map(|n| match n {
      Node::CodeBlock(cb) => Some(cb),
      _ => None,
    })
    .expect("cb");
  assert!(cb.value.contains("export const x = 1"), "got {:?}", cb.value);
}