radicle-source 0.1.0

A high level API for browsing source files
Documentation
// This file is part of radicle-surf
// <https://github.com/radicle-dev/radicle-surf>
//
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 or
// later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use std::{
    convert::TryFrom as _,
    str::{self, FromStr as _},
};

use serde::{
    ser::{SerializeStruct as _, Serializer},
    Serialize,
};

use radicle_surf::{
    file_system,
    vcs::git::{Browser, Rev},
};

use crate::{
    commit,
    error::Error,
    object::{Info, ObjectType},
    revision::Revision,
};

#[cfg(feature = "syntax")]
use crate::syntax;

/// File data abstraction.
pub struct Blob {
    /// Actual content of the file, if the content is ASCII.
    pub content: BlobContent,
    /// Extra info for the file.
    pub info: Info,
    /// Absolute path to the object from the root of the repo.
    pub path: String,
}

impl Blob {
    /// Indicates if the content of the [`Blob`] is binary.
    #[must_use]
    pub fn is_binary(&self) -> bool {
        self.content == BlobContent::Binary
    }

    /// Indicates if the content of the [`Blob`] is HTML.
    #[must_use]
    pub const fn is_html(&self) -> bool {
        matches!(self.content, BlobContent::Html(_))
    }
}

impl Serialize for Blob {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("Blob", 5)?;
        state.serialize_field("binary", &self.is_binary())?;
        state.serialize_field("html", &self.is_html())?;
        state.serialize_field("content", &self.content)?;
        state.serialize_field("info", &self.info)?;
        state.serialize_field("path", &self.path)?;
        state.end()
    }
}

/// Variants of blob content.
#[derive(PartialEq)]
pub enum BlobContent {
    /// Content is ASCII and can be passed as a string.
    Ascii(String),
    /// Content is syntax-highlighted HTML.
    ///
    /// Note that is necessary to enable the `syntax` feature flag for this
    /// variant to be constructed. Use `highlighting::blob`, instead of
    /// [`blob`] to get highlighted content.
    Html(String),
    /// Content is binary and needs special treatment.
    Binary,
}

impl Serialize for BlobContent {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Self::Ascii(content) | Self::Html(content) => serializer.serialize_str(content),
            Self::Binary => serializer.serialize_none(),
        }
    }
}

/// Returns the [`Blob`] for a file at `revision` under `path`.
///
/// # Errors
///
/// Will return [`Error`] if the project doesn't exist or a surf interaction
/// fails.
pub fn blob<P>(
    browser: &mut Browser,
    maybe_revision: Option<Revision<P>>,
    path: &str,
) -> Result<Blob, Error>
where
    P: ToString,
{
    make_blob(browser, maybe_revision, path, ascii)
}

fn make_blob<P, C>(
    browser: &mut Browser,
    maybe_revision: Option<Revision<P>>,
    path: &str,
    content: C,
) -> Result<Blob, Error>
where
    P: ToString,
    C: FnOnce(&[u8]) -> BlobContent,
{
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
    if let Some(revision) = maybe_revision {
        browser.rev(revision)?;
    }

    let root = browser.get_directory()?;
    let p = file_system::Path::from_str(path)?;

    let file = root
        .find_file(p.clone())
        .ok_or_else(|| Error::PathNotFound(p.clone()))?;

    let mut commit_path = file_system::Path::root();
    commit_path.append(p.clone());

    let last_commit = browser
        .last_commit(commit_path)?
        .map(|c| commit::Header::from(&c));
    let (_rest, last) = p.split_last();

    let content = content(&file.contents);

    Ok(Blob {
        content,
        info: Info {
            name: last.to_string(),
            object_type: ObjectType::Blob,
            last_commit,
        },
        path: path.to_string(),
    })
}

/// Return a [`BlobContent`] given a file path, content and theme. Attempts to
/// perform syntax highlighting when the theme is `Some`.
fn ascii(content: &[u8]) -> BlobContent {
    match str::from_utf8(content) {
        Ok(content) => BlobContent::Ascii(content.to_owned()),
        Err(_) => BlobContent::Binary,
    }
}

#[cfg(feature = "syntax")]
pub mod highlighting {
    use super::*;

    /// Returns the [`Blob`] for a file at `revision` under `path`.
    ///
    /// # Errors
    ///
    /// Will return [`Error`] if the project doesn't exist or a surf interaction
    /// fails.
    pub fn blob<P>(
        browser: &mut Browser,
        maybe_revision: Option<Revision<P>>,
        path: &str,
        theme: Option<&str>,
    ) -> Result<Blob, Error>
    where
        P: ToString,
    {
        make_blob(browser, maybe_revision, path, |contents| {
            content(path, contents, theme)
        })
    }

    /// Return a [`BlobContent`] given a file path, content and theme. Attempts
    /// to perform syntax highlighting when the theme is `Some`.
    fn content(path: &str, content: &[u8], theme_name: Option<&str>) -> BlobContent {
        let content = match str::from_utf8(content) {
            Ok(content) => content,
            Err(_) => return BlobContent::Binary,
        };

        match theme_name {
            None => BlobContent::Ascii(content.to_owned()),
            Some(theme) => syntax::highlight(path, content, theme)
                .map_or_else(|| BlobContent::Ascii(content.to_owned()), BlobContent::Html),
        }
    }
}