1use std::{
2 fs,
3 io::{self, IsTerminal, Read},
4 path::{Path, PathBuf},
5};
6
7use anyhow::{Context, Result, bail};
8
9use crate::cli::FormatArg;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InputKind {
13 Text,
14 Markdown,
15}
16
17impl InputKind {
18 pub fn label(self) -> &'static str {
19 match self {
20 Self::Text => "Text",
21 Self::Markdown => "Markdown",
22 }
23 }
24}
25
26#[derive(Debug)]
27pub struct Input {
28 pub content: String,
29 pub name: String,
30 pub kind: InputKind,
31 pub base_dir: Option<PathBuf>,
32}
33
34pub fn read_input(path: Option<&Path>, format: FormatArg) -> Result<Input> {
35 match path {
36 Some(path) if path != Path::new("-") => read_file(path, format),
37 Some(_) => read_stdin(format),
38 None if !io::stdin().is_terminal() => read_stdin(format),
39 None => bail!("no input provided (try 'iris <file>' or pipe data into iris)"),
40 }
41}
42
43fn read_file(path: &Path, format: FormatArg) -> Result<Input> {
44 if !path.exists() {
45 bail!("file not found: {}", path.display());
46 }
47 if path.is_dir() {
48 bail!("cannot read directory: {}", path.display());
49 }
50
51 let bytes = fs::read(path).with_context(|| format!("failed to read '{}'", path.display()))?;
52 let content = String::from_utf8(bytes)
53 .map_err(|_| anyhow::anyhow!("input is not valid UTF-8: {}", path.display()))?;
54 let name = path
55 .file_name()
56 .and_then(|value| value.to_str())
57 .unwrap_or_else(|| path.to_str().unwrap_or("input"))
58 .to_string();
59
60 Ok(Input {
61 content,
62 name,
63 kind: resolve_kind(Some(path), format),
64 base_dir: path.parent().map(Path::to_path_buf),
65 })
66}
67
68fn read_stdin(format: FormatArg) -> Result<Input> {
69 let mut bytes = Vec::new();
70 io::stdin()
71 .lock()
72 .read_to_end(&mut bytes)
73 .context("failed to read stdin")?;
74 let content =
75 String::from_utf8(bytes).map_err(|_| anyhow::anyhow!("stdin is not valid UTF-8"))?;
76
77 Ok(Input {
78 content,
79 name: "stdin".to_string(),
80 kind: resolve_kind(None, format),
81 base_dir: None,
82 })
83}
84
85fn resolve_kind(path: Option<&Path>, format: FormatArg) -> InputKind {
86 match format {
87 FormatArg::Text => InputKind::Text,
88 FormatArg::Markdown => InputKind::Markdown,
89 FormatArg::Auto => path.map(detect_kind).unwrap_or(InputKind::Text),
90 }
91}
92
93fn detect_kind(path: &Path) -> InputKind {
94 match path
95 .extension()
96 .and_then(|ext| ext.to_str())
97 .map(str::to_ascii_lowercase)
98 .as_deref()
99 {
100 Some("md" | "markdown" | "mdown" | "mkd" | "mkdn") => InputKind::Markdown,
101 _ => InputKind::Text,
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn detects_markdown_extensions() {
111 assert_eq!(
112 detect_kind(&std::path::PathBuf::from("README.md")),
113 InputKind::Markdown
114 );
115 assert_eq!(
116 detect_kind(&std::path::PathBuf::from("notes.txt")),
117 InputKind::Text
118 );
119 }
120}