Skip to main content

mdzip_core/
entrypoint.rs

1//! Entry point discovery (Section 5.5).
2
3use crate::error::{ErrorCode, MdzError, Result};
4use crate::manifest::{Manifest, entry_point_value_problem};
5use crate::paths::{find_path, is_markdown, is_support_file, strip_dot_slash};
6
7/// The archive's primary Markdown file, by the ordered algorithm in Section 5.5.
8///
9/// 1. A manifest `entryPoint` is used if it's a valid entry point path that
10///    matches a file. Otherwise this fails with
11///    [`ErrorCode::EntryPointInvalid`] or [`ErrorCode::EntryPointMissing`];
12///    it never falls back silently.
13/// 2. `index.md` at the archive root.
14/// 3. The single root-level Markdown file, setting support files aside; or,
15///    if every root-level Markdown file is a support file and there's exactly
16///    one, that file.
17/// 4. Otherwise [`ErrorCode::EntryPointUnresolved`].
18///
19/// `paths` are the archive's entry names; directory entries (ending in `/`)
20/// are ignored. Returns the path as stored.
21pub fn resolve_entry_point<'a, I>(paths: I, manifest: Option<&Manifest>) -> Result<String>
22where
23    I: IntoIterator<Item = &'a str>,
24{
25    let files: Vec<&str> = paths.into_iter().filter(|p| !p.ends_with('/')).collect();
26
27    if let Some(value) = manifest.and_then(|m| m.get("entryPoint")) {
28        if let Some(problem) = entry_point_value_problem(value) {
29            return Err(MdzError::new(ErrorCode::EntryPointInvalid, problem));
30        }
31        let wanted = value.as_str().unwrap_or_default();
32        return find_path(files.iter().copied(), strip_dot_slash(wanted))
33            .map(str::to_owned)
34            .ok_or_else(|| {
35                MdzError::new(
36                    ErrorCode::EntryPointMissing,
37                    format!("entryPoint '{wanted}' is not in the archive"),
38                )
39            });
40    }
41
42    if let Some(index) = find_path(files.iter().copied(), "index.md") {
43        return Ok(index.to_owned());
44    }
45
46    let root_markdown: Vec<&str> = files
47        .iter()
48        .copied()
49        .filter(|p| !p.contains('/') && is_markdown(p))
50        .collect();
51    let content: Vec<&str> = root_markdown.iter().copied().filter(|p| !is_support_file(p)).collect();
52    if content.len() == 1 {
53        return Ok(content[0].to_owned());
54    }
55    if content.is_empty() && root_markdown.len() == 1 {
56        return Ok(root_markdown[0].to_owned());
57    }
58    if root_markdown.is_empty() {
59        return Err(MdzError::new(
60            ErrorCode::EntryPointUnresolved,
61            "no Markdown file at the archive root",
62        ));
63    }
64    let mut names = root_markdown;
65    names.sort_unstable();
66    Err(MdzError::new(
67        ErrorCode::EntryPointUnresolved,
68        format!(
69            "several root Markdown files and no index.md or manifest entryPoint: {}",
70            names.join(", ")
71        ),
72    ))
73}