mirror of https://github.com/realaravinth/gitpad
feat: gist view
parent
9fb203de32
commit
2323dbf82c
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::path::Path;
|
||||
|
||||
use syntect::highlighting::{Color, ThemeSet};
|
||||
use syntect::html::highlighted_html_for_string;
|
||||
use syntect::parsing::{SyntaxReference, SyntaxSet};
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
pub trait GenerateHTML {
|
||||
fn generate(&mut self);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const STYLE: &str = "
|
||||
";
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines();
|
||||
}
|
||||
|
||||
pub struct SourcegraphQuery<'a> {
|
||||
pub filepath: &'a str,
|
||||
pub code: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> SourcegraphQuery<'a> {
|
||||
pub fn syntax_highlight(&self) -> String {
|
||||
// let ss = SYNTAX_SET;
|
||||
let ts = ThemeSet::load_defaults();
|
||||
|
||||
let theme = &ts.themes["InspiredGitHub"];
|
||||
let c = theme.settings.background.unwrap_or(Color::WHITE);
|
||||
let mut num = 1;
|
||||
let mut output = format!(
|
||||
"<div style=\"background-color:#{:02x}{:02x}{:02x};\">\n",
|
||||
c.r, c.g, c.b
|
||||
);
|
||||
|
||||
// highlighted_html_for_string(&q.code, syntax_set, syntax_def, theme),
|
||||
let html = SYNTAX_SET.with(|ss| {
|
||||
let language = self.determine_language(ss).unwrap();
|
||||
highlighted_html_for_string(self.code, ss, language, theme)
|
||||
});
|
||||
let total_lines = html.lines().count();
|
||||
for (line_num, line) in html.lines().enumerate() {
|
||||
if !line.trim().is_empty() {
|
||||
if line_num == 0 || line_num == total_lines - 1 {
|
||||
output.push_str(line);
|
||||
} else {
|
||||
output.push_str(&format!("<div title='click for more options' id=\"line-{num}\"class=\"line\"><details class='line_links'><summary class='line_top-link'><a href=\"#line-{num}\"<span class=\"line-number\">{num}</span></a>{line}</summary><a href=\"#line-{num}\"<span class=\"line-link\">Permanant link</span></a><a href=\"#line-{num}\"<span class=\"line-link\">Highlight</span></a></details></div>"
|
||||
));
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push_str("</div>");
|
||||
output
|
||||
}
|
||||
|
||||
// adopted from
|
||||
// https://github.com/sourcegraph/sourcegraph/blob/9fe138ae75fd64dce06b621572b252a9c9c8da70/docker-images/syntax-highlighter/crates/sg-syntax/src/lib.rs#L81
|
||||
// with minimum modifications. Crate was MIT licensed at the time(2022-03-12 11:11)
|
||||
fn determine_language<'b>(
|
||||
&self,
|
||||
syntax_set: &'b SyntaxSet,
|
||||
) -> ServiceResult<&'b SyntaxReference> {
|
||||
if self.filepath.is_empty() {
|
||||
// Legacy codepath, kept for backwards-compatability with old clients.
|
||||
match syntax_set.find_syntax_by_first_line(self.code) {
|
||||
Some(v) => {
|
||||
return Ok(v);
|
||||
}
|
||||
None => unimplemented!(), //Err(json!({"error": "invalid extension"})),
|
||||
};
|
||||
}
|
||||
|
||||
// Split the input path ("foo/myfile.go") into file name
|
||||
// ("myfile.go") and extension ("go").
|
||||
let path = Path::new(&self.filepath);
|
||||
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
|
||||
|
||||
// Override syntect's language detection for conflicting file extensions because
|
||||
// it's impossible to express this logic in a syntax definition.
|
||||
struct Override {
|
||||
extension: &'static str,
|
||||
prefix_langs: Vec<(&'static str, &'static str)>,
|
||||
default: &'static str,
|
||||
}
|
||||
let overrides = vec![Override {
|
||||
extension: "cls",
|
||||
prefix_langs: vec![("%", "TeX"), ("\\", "TeX")],
|
||||
default: "Apex",
|
||||
}];
|
||||
|
||||
if let Some(Override {
|
||||
prefix_langs,
|
||||
default,
|
||||
..
|
||||
}) = overrides.iter().find(|o| o.extension == extension)
|
||||
{
|
||||
let name = match prefix_langs
|
||||
.iter()
|
||||
.find(|(prefix, _)| self.code.starts_with(prefix))
|
||||
{
|
||||
Some((_, lang)) => lang,
|
||||
None => default,
|
||||
};
|
||||
return Ok(syntax_set
|
||||
.find_syntax_by_name(name)
|
||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text()));
|
||||
}
|
||||
|
||||
Ok(syntax_set
|
||||
// First try to find a syntax whose "extension" matches our file
|
||||
// name. This is done due to some syntaxes matching an "extension"
|
||||
// that is actually a whole file name (e.g. "Dockerfile" or "CMakeLists.txt")
|
||||
// see https://github.com/trishume/syntect/pull/170
|
||||
.find_syntax_by_extension(file_name)
|
||||
.or_else(|| syntax_set.find_syntax_by_extension(extension))
|
||||
.or_else(|| syntax_set.find_syntax_by_first_line(self.code))
|
||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SourcegraphQuery;
|
||||
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
#[test]
|
||||
fn cls_tex() {
|
||||
let syntax_set = SyntaxSet::load_defaults_newlines();
|
||||
let query = SourcegraphQuery {
|
||||
filepath: "foo.cls",
|
||||
code: "%",
|
||||
};
|
||||
let result = query.determine_language(&syntax_set);
|
||||
assert_eq!(result.unwrap().name, "TeX");
|
||||
let _result = query.syntax_highlight();
|
||||
}
|
||||
|
||||
//#[test]
|
||||
//fn cls_apex() {
|
||||
// let syntax_set = SyntaxSet::load_defaults_newlines();
|
||||
// let query = SourcegraphQuery {
|
||||
// filepath: "foo.cls".to_string(),
|
||||
// code: "/**".to_string(),
|
||||
// extension: String::new(),
|
||||
// };
|
||||
// let result = determine_language(&query, &syntax_set);
|
||||
// assert_eq!(result.unwrap().name, "Apex");
|
||||
//}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::cell::RefCell;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header::ContentType;
|
||||
use tera::Context;
|
||||
|
||||
use db_core::prelude::*;
|
||||
|
||||
use crate::data::api::v1::gists::{GistID, GistInfo};
|
||||
use crate::data::api::v1::render_html::GenerateHTML;
|
||||
use crate::errors::*;
|
||||
use crate::pages::routes::GistProfilePathComponent;
|
||||
use crate::pages::routes::PostCommentPath;
|
||||
use crate::settings::Settings;
|
||||
use crate::AppData;
|
||||
|
||||
pub use super::*;
|
||||
|
||||
pub const VIEW_GIST: TemplateFile = TemplateFile::new("viewgist", "pages/gists/view/index.html");
|
||||
pub const GIST_TEXTFILE: TemplateFile =
|
||||
TemplateFile::new("gist_textfile", "pages/gists/view/_text.html");
|
||||
pub const GIST_FILENAME: TemplateFile =
|
||||
TemplateFile::new("gist_filename", "pages/gists/view/_filename.html");
|
||||
|
||||
pub fn register_templates(t: &mut tera::Tera) {
|
||||
for template in [VIEW_GIST, GIST_FILENAME, GIST_TEXTFILE].iter() {
|
||||
template.register(t).expect(template.name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(view_preview);
|
||||
}
|
||||
|
||||
pub struct ViewGist {
|
||||
ctx: RefCell<Context>,
|
||||
}
|
||||
|
||||
impl CtxError for ViewGist {
|
||||
fn with_error(&self, e: &ReadableError) -> String {
|
||||
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewGist {
|
||||
pub fn new(username: Option<&str>, gist: Option<&GistInfo>, settings: &Settings) -> Self {
|
||||
let mut ctx = auth_ctx(username, settings);
|
||||
ctx.insert("visibility_private", GistVisibility::Private.to_str());
|
||||
ctx.insert("visibility_unlisted", GistVisibility::Unlisted.to_str());
|
||||
ctx.insert("visibility_public", GistVisibility::Public.to_str());
|
||||
|
||||
if let Some(gist) = gist {
|
||||
ctx.insert(PAYLOAD_KEY, gist);
|
||||
ctx.insert(
|
||||
"gist_owner_link",
|
||||
&PAGES.gist.get_profile_route(GistProfilePathComponent {
|
||||
username: &gist.owner,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let ctx = RefCell::new(ctx);
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
TEMPLATES
|
||||
.render(VIEW_GIST.name, &self.ctx.borrow())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn page(username: Option<&str>, gist: Option<&GistInfo>, s: &Settings) -> String {
|
||||
let p = Self::new(username, gist, s);
|
||||
p.render()
|
||||
}
|
||||
}
|
||||
#[my_codegen::get(path = "PAGES.gist.view_gist", wrap = "super::get_auth_middleware()")]
|
||||
async fn view_preview(
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
id: Identity,
|
||||
path: web::Path<PostCommentPath>,
|
||||
) -> PageResult<impl Responder, ViewGist> {
|
||||
let username = id.identity();
|
||||
|
||||
let map_err = |e: ServiceError, g: Option<&GistInfo>| -> PageError<ViewGist> {
|
||||
PageError::new(ViewGist::new(username.as_deref(), g, &data.settings), e)
|
||||
};
|
||||
|
||||
let gist = db.get_gist(&path.gist).await.map_err(|e| {
|
||||
let err: ServiceError = e.into();
|
||||
map_err(err, None)
|
||||
})?;
|
||||
|
||||
if let Some(username) = &username {
|
||||
if gist.visibility == GistVisibility::Private && username != &gist.owner {
|
||||
return Err(map_err(ServiceError::GistNotFound, None));
|
||||
}
|
||||
}
|
||||
|
||||
let mut gist = data
|
||||
.gist_preview(db.as_ref(), &mut GistID::ID(&path.gist))
|
||||
.await
|
||||
.map_err(|e| map_err(e, None))?;
|
||||
|
||||
gist.files.iter_mut().for_each(|file| file.generate());
|
||||
|
||||
let page = ViewGist::page(username.as_deref(), Some(&gist), &data.settings);
|
||||
let html = ContentType::html();
|
||||
Ok(HttpResponse::Ok().content_type(html).body(page))
|
||||
}
|
@ -0,0 +1 @@
|
||||
<div class="gist__filename">{{ payload_file.filename }}</div>
|
@ -0,0 +1,3 @@
|
||||
{% if "text" in payload_file.content.file %}
|
||||
{{ payload_file.content.file.text }}
|
||||
{% endif %}
|
@ -0,0 +1,31 @@
|
||||
{% extends 'gistbase' %}
|
||||
{% block gist_main %}
|
||||
{% include "error_comp" %}
|
||||
<div class="gist__container>
|
||||
{% if payload %}
|
||||
|
||||
<p class="gist__name">
|
||||
<a href={{ gist_owner_link }}>{{ payload.owner }}</a>/<a href="">{{ payload.id | truncate(length=10) }}</a>
|
||||
<span class="gist__visibility">{{ payload.visibility}}</span>
|
||||
</p>
|
||||
|
||||
{% if payload.description %}
|
||||
<p class="gist__description">{{ payload.description}}</p>
|
||||
{% endif %}
|
||||
{% for payload_file in payload.files %}
|
||||
{% include "gist_filename" %}
|
||||
{% if "file" in payload_file.content %}
|
||||
{% include "gist_textfile" %}
|
||||
{% endif %}
|
||||
|
||||
{% if "dir" in payload_file.content %}
|
||||
{% for payload_file in payload_file.content.dir %}
|
||||
{% include "gist_filename" %}
|
||||
{% include "gist_textfile" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue