Initial commit
This commit is contained in:
commit
6133cf9a46
16 changed files with 2050 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
8
.travis.yml
Normal file
8
.travis.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
script:
|
||||
- cargo build --verbose --all
|
||||
- cargo test --verbose --all
|
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "pastebin"
|
||||
version = "0.1.0"
|
||||
authors = ["Kaczanowski Mateusz <kaczanowski.mateusz@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.4.4", features = ["tls"] }
|
||||
rocksdb = "0.13.0"
|
||||
nanoid = "0.3.0"
|
||||
flatbuffers = "0.6.1"
|
||||
structopt = "0.1.3"
|
||||
structopt-derive = "0.1.3"
|
||||
num_cpus = "1.0"
|
||||
handlebars = "3.0.1"
|
||||
maplit = "1.0.2"
|
||||
tempdir = "0.3.7"
|
||||
speculate = "0.1.2"
|
||||
chrono = "0.4.11"
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
FROM rustlang/rust:nightly
|
||||
|
||||
RUN apt-get update && apt-get install -y apt-utils software-properties-common lsb-release
|
||||
RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
|
||||
|
||||
WORKDIR /usr/src/pastebin
|
||||
COPY . .
|
||||
|
||||
RUN cargo install --path .
|
||||
|
||||
CMD ["pastebin"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mateusz Kaczanowski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
[![Build Status][travis-badge]][travis]
|
||||
|
||||
[travis-badge]: https://travis-ci.org/mkaczanowski/packer-builder-arm.svg?branch=master
|
||||
|
||||
# Pastebin
|
||||
Simple, fast and standalone pastebin service
|
||||
|
12
api.fbs
Normal file
12
api.fbs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace api;
|
||||
|
||||
table Entry {
|
||||
create_timestamp:ulong;
|
||||
expiry_timestamp:ulong;
|
||||
data:[ubyte];
|
||||
lang:string;
|
||||
burn:bool;
|
||||
encrypted:bool;
|
||||
}
|
||||
|
||||
root_type Entry;
|
180
src/api_generated.rs
Normal file
180
src/api_generated.rs
Normal file
|
@ -0,0 +1,180 @@
|
|||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
#![allow(warnings)]
|
||||
#![allow(clippy)]
|
||||
#![allow(unknown_lints)]
|
||||
|
||||
use std::mem;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
extern crate flatbuffers;
|
||||
use self::flatbuffers::EndianScalar;
|
||||
|
||||
#[allow(unused_imports, dead_code)]
|
||||
pub mod api {
|
||||
|
||||
use std::mem;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
extern crate flatbuffers;
|
||||
use self::flatbuffers::EndianScalar;
|
||||
|
||||
pub enum EntryOffset {}
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
|
||||
pub struct Entry<'a> {
|
||||
pub _tab: flatbuffers::Table<'a>,
|
||||
}
|
||||
|
||||
impl<'a> flatbuffers::Follow<'a> for Entry<'a> {
|
||||
type Inner = Entry<'a>;
|
||||
#[inline]
|
||||
fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {
|
||||
Self {
|
||||
_tab: flatbuffers::Table { buf: buf, loc: loc },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Entry<'a> {
|
||||
#[inline]
|
||||
pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self {
|
||||
Entry {
|
||||
_tab: table,
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>(
|
||||
_fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>,
|
||||
args: &'args EntryArgs<'args>) -> flatbuffers::WIPOffset<Entry<'bldr>> {
|
||||
let mut builder = EntryBuilder::new(_fbb);
|
||||
builder.add_expiry_timestamp(args.expiry_timestamp);
|
||||
builder.add_create_timestamp(args.create_timestamp);
|
||||
if let Some(x) = args.lang { builder.add_lang(x); }
|
||||
if let Some(x) = args.data { builder.add_data(x); }
|
||||
builder.add_encrypted(args.encrypted);
|
||||
builder.add_burn(args.burn);
|
||||
builder.finish()
|
||||
}
|
||||
|
||||
pub const VT_CREATE_TIMESTAMP: flatbuffers::VOffsetT = 4;
|
||||
pub const VT_EXPIRY_TIMESTAMP: flatbuffers::VOffsetT = 6;
|
||||
pub const VT_DATA: flatbuffers::VOffsetT = 8;
|
||||
pub const VT_LANG: flatbuffers::VOffsetT = 10;
|
||||
pub const VT_BURN: flatbuffers::VOffsetT = 12;
|
||||
pub const VT_ENCRYPTED: flatbuffers::VOffsetT = 14;
|
||||
|
||||
#[inline]
|
||||
pub fn create_timestamp(&self) -> u64 {
|
||||
self._tab.get::<u64>(Entry::VT_CREATE_TIMESTAMP, Some(0)).unwrap()
|
||||
}
|
||||
#[inline]
|
||||
pub fn expiry_timestamp(&self) -> u64 {
|
||||
self._tab.get::<u64>(Entry::VT_EXPIRY_TIMESTAMP, Some(0)).unwrap()
|
||||
}
|
||||
#[inline]
|
||||
pub fn data(&self) -> Option<&'a [u8]> {
|
||||
self._tab.get::<flatbuffers::ForwardsUOffset<flatbuffers::Vector<'a, u8>>>(Entry::VT_DATA, None).map(|v| v.safe_slice())
|
||||
}
|
||||
#[inline]
|
||||
pub fn lang(&self) -> Option<&'a str> {
|
||||
self._tab.get::<flatbuffers::ForwardsUOffset<&str>>(Entry::VT_LANG, None)
|
||||
}
|
||||
#[inline]
|
||||
pub fn burn(&self) -> bool {
|
||||
self._tab.get::<bool>(Entry::VT_BURN, Some(false)).unwrap()
|
||||
}
|
||||
#[inline]
|
||||
pub fn encrypted(&self) -> bool {
|
||||
self._tab.get::<bool>(Entry::VT_ENCRYPTED, Some(false)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EntryArgs<'a> {
|
||||
pub create_timestamp: u64,
|
||||
pub expiry_timestamp: u64,
|
||||
pub data: Option<flatbuffers::WIPOffset<flatbuffers::Vector<'a , u8>>>,
|
||||
pub lang: Option<flatbuffers::WIPOffset<&'a str>>,
|
||||
pub burn: bool,
|
||||
pub encrypted: bool,
|
||||
}
|
||||
impl<'a> Default for EntryArgs<'a> {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
EntryArgs {
|
||||
create_timestamp: 0,
|
||||
expiry_timestamp: 0,
|
||||
data: None,
|
||||
lang: None,
|
||||
burn: false,
|
||||
encrypted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct EntryBuilder<'a: 'b, 'b> {
|
||||
fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>,
|
||||
start_: flatbuffers::WIPOffset<flatbuffers::TableUnfinishedWIPOffset>,
|
||||
}
|
||||
impl<'a: 'b, 'b> EntryBuilder<'a, 'b> {
|
||||
#[inline]
|
||||
pub fn add_create_timestamp(&mut self, create_timestamp: u64) {
|
||||
self.fbb_.push_slot::<u64>(Entry::VT_CREATE_TIMESTAMP, create_timestamp, 0);
|
||||
}
|
||||
#[inline]
|
||||
pub fn add_expiry_timestamp(&mut self, expiry_timestamp: u64) {
|
||||
self.fbb_.push_slot::<u64>(Entry::VT_EXPIRY_TIMESTAMP, expiry_timestamp, 0);
|
||||
}
|
||||
#[inline]
|
||||
pub fn add_data(&mut self, data: flatbuffers::WIPOffset<flatbuffers::Vector<'b , u8>>) {
|
||||
self.fbb_.push_slot_always::<flatbuffers::WIPOffset<_>>(Entry::VT_DATA, data);
|
||||
}
|
||||
#[inline]
|
||||
pub fn add_lang(&mut self, lang: flatbuffers::WIPOffset<&'b str>) {
|
||||
self.fbb_.push_slot_always::<flatbuffers::WIPOffset<_>>(Entry::VT_LANG, lang);
|
||||
}
|
||||
#[inline]
|
||||
pub fn add_burn(&mut self, burn: bool) {
|
||||
self.fbb_.push_slot::<bool>(Entry::VT_BURN, burn, false);
|
||||
}
|
||||
#[inline]
|
||||
pub fn add_encrypted(&mut self, encrypted: bool) {
|
||||
self.fbb_.push_slot::<bool>(Entry::VT_ENCRYPTED, encrypted, false);
|
||||
}
|
||||
#[inline]
|
||||
pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> EntryBuilder<'a, 'b> {
|
||||
let start = _fbb.start_table();
|
||||
EntryBuilder {
|
||||
fbb_: _fbb,
|
||||
start_: start,
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn finish(self) -> flatbuffers::WIPOffset<Entry<'a>> {
|
||||
let o = self.fbb_.end_table(self.start_);
|
||||
flatbuffers::WIPOffset::new(o.value())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_root_as_entry<'a>(buf: &'a [u8]) -> Entry<'a> {
|
||||
flatbuffers::get_root::<Entry<'a>>(buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_size_prefixed_root_as_entry<'a>(buf: &'a [u8]) -> Entry<'a> {
|
||||
flatbuffers::get_size_prefixed_root::<Entry<'a>>(buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finish_entry_buffer<'a, 'b>(
|
||||
fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>,
|
||||
root: flatbuffers::WIPOffset<Entry<'a>>) {
|
||||
fbb.finish(root, None);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finish_size_prefixed_entry_buffer<'a, 'b>(fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, root: flatbuffers::WIPOffset<Entry<'a>>) {
|
||||
fbb.finish_size_prefixed(root, None);
|
||||
}
|
||||
} // pub mod api
|
||||
|
129
src/lib.rs
Normal file
129
src/lib.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
extern crate flatbuffers;
|
||||
|
||||
use std::io;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use flatbuffers::FlatBufferBuilder;
|
||||
use rocket::State;
|
||||
use rocksdb::{compaction_filter, DB};
|
||||
|
||||
#[path = "api_generated.rs"]
|
||||
mod api_generated;
|
||||
use crate::api_generated::api::{finish_entry_buffer, get_root_as_entry, Entry, EntryArgs};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! load_static_resources {
|
||||
( $( $x:expr ),* ) => {
|
||||
{
|
||||
let mut resources: HashMap<&str, &[u8]> = HashMap::new();
|
||||
$(
|
||||
resources.insert($x, include_bytes!($x));
|
||||
)*
|
||||
|
||||
resources
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn compaction_filter_expired_entries(
|
||||
_: u32,
|
||||
_: &[u8],
|
||||
value: &[u8],
|
||||
) -> compaction_filter::Decision {
|
||||
use compaction_filter::Decision::*;
|
||||
|
||||
let entry = get_root_as_entry(value);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("time went backwards")
|
||||
.as_secs();
|
||||
|
||||
if entry.expiry_timestamp() != 0 && now >= entry.expiry_timestamp() {
|
||||
Remove
|
||||
} else {
|
||||
Keep
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(filename: &str) -> &str {
|
||||
filename
|
||||
.rfind('.')
|
||||
.map(|idx| &filename[idx..])
|
||||
.filter(|ext| ext.chars().skip(1).all(|c| c.is_ascii_alphanumeric()))
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn get_entry_data<'r>(id: &str, state: &'r State<'r, DB>) -> Result<Vec<u8>, io::Error> {
|
||||
// read data from DB to Entry struct
|
||||
let root = match state.get(id).unwrap() {
|
||||
Some(root) => root,
|
||||
None => return Err(io::Error::new(io::ErrorKind::NotFound, "record not found")),
|
||||
};
|
||||
let entry = get_root_as_entry(&root);
|
||||
|
||||
// check if data expired (might not be yet deleted by rocksb compaction hook)
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("time went backwards")
|
||||
.as_secs();
|
||||
|
||||
if entry.expiry_timestamp() != 0 && now >= entry.expiry_timestamp() {
|
||||
state.delete(id).unwrap();
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound, "record not found"));
|
||||
}
|
||||
|
||||
// "burn" one time only pastebin content
|
||||
if entry.burn() {
|
||||
state.delete(id).unwrap();
|
||||
}
|
||||
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
pub fn new_entry(
|
||||
dest: &mut Vec<u8>,
|
||||
data: &mut rocket::data::DataStream,
|
||||
lang: String,
|
||||
ttl: u64,
|
||||
burn: bool,
|
||||
encrypted: bool,
|
||||
) {
|
||||
let mut bldr = FlatBufferBuilder::new();
|
||||
|
||||
dest.clear();
|
||||
bldr.reset();
|
||||
|
||||
// potential speed improvement over the create_vector:
|
||||
// https://docs.rs/flatbuffers/0.6.1/flatbuffers/struct.FlatBufferBuilder.html#method.create_vector
|
||||
let mut tmp_vec: Vec<u8> = vec![];
|
||||
std::io::copy(data, &mut tmp_vec).unwrap();
|
||||
|
||||
bldr.start_vector::<u8>(tmp_vec.len());
|
||||
for byte in tmp_vec.iter().rev() {
|
||||
bldr.push::<u8>(*byte);
|
||||
}
|
||||
let data_vec = bldr.end_vector::<u8>(tmp_vec.len());
|
||||
|
||||
// calc expiry datetime
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("time went backwards")
|
||||
.as_secs();
|
||||
let expiry = if ttl == 0 { ttl } else { now + ttl };
|
||||
|
||||
// setup actual struct
|
||||
let args = EntryArgs {
|
||||
create_timestamp: now,
|
||||
expiry_timestamp: expiry,
|
||||
data: Some(data_vec),
|
||||
lang: Some(bldr.create_string(&lang)),
|
||||
burn,
|
||||
encrypted,
|
||||
};
|
||||
|
||||
let user_offset = Entry::create(&mut bldr, &args);
|
||||
finish_entry_buffer(&mut bldr, user_offset);
|
||||
|
||||
let finished_data = bldr.finished_data();
|
||||
dest.extend_from_slice(finished_data);
|
||||
}
|
569
src/main.rs
Normal file
569
src/main.rs
Normal file
|
@ -0,0 +1,569 @@
|
|||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate structopt_derive;
|
||||
#[macro_use]
|
||||
extern crate maplit;
|
||||
extern crate flatbuffers;
|
||||
extern crate handlebars;
|
||||
extern crate nanoid;
|
||||
extern crate num_cpus;
|
||||
extern crate speculate;
|
||||
extern crate structopt;
|
||||
extern crate chrono;
|
||||
|
||||
#[macro_use]
|
||||
mod lib;
|
||||
use lib::{compaction_filter_expired_entries, get_entry_data, get_extension, new_entry};
|
||||
|
||||
mod api_generated;
|
||||
use api_generated::api::get_root_as_entry;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
|
||||
use rocket::config::{Config, Environment};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::response::{Redirect, Response};
|
||||
use rocket::{Data, State};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use nanoid::nanoid;
|
||||
use rocksdb::{Options, DB};
|
||||
use structopt::StructOpt;
|
||||
use chrono::NaiveDateTime;
|
||||
use speculate::speculate;
|
||||
|
||||
speculate! {
|
||||
use super::rocket;
|
||||
use rocket::local::Client;
|
||||
use rocket::http::Status;
|
||||
|
||||
before {
|
||||
use tempdir::TempDir;
|
||||
|
||||
// setup temporary database
|
||||
let tmp_dir = TempDir::new("rocks_db_test").unwrap();
|
||||
let file_path = tmp_dir.path().join("database");
|
||||
let mut pastebin_config = PastebinConfig::from_args();
|
||||
pastebin_config.db_path = file_path.to_str().unwrap().to_string();
|
||||
let rocket = rocket(pastebin_config);
|
||||
|
||||
// init rocket client
|
||||
let client = Client::new(rocket).expect("invalid rocket instance");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn insert_data<'r>(client: &'r Client, data: &str, path: &str) -> String {
|
||||
let mut response = client.post(path)
|
||||
.body(data)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
// retrieve paste ID
|
||||
let url = response.body_string().unwrap();
|
||||
let id = url.split('/').collect::<Vec<&str>>().last().cloned().unwrap();
|
||||
|
||||
id.to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_data(client: &Client, path: String) -> rocket::local::LocalResponse {
|
||||
client.get(format!("/{}", path)).dispatch()
|
||||
}
|
||||
|
||||
it "can get create and fetch paste" {
|
||||
// store data via post request
|
||||
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
|
||||
|
||||
// retrieve the data via get request
|
||||
let mut response = get_data(&client, id);
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
|
||||
}
|
||||
|
||||
it "can remove paste by id" {
|
||||
let response = client.delete("/some_id").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let response = get_data(&client, "some_id".to_string());
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
|
||||
it "can remove non-existing paste" {
|
||||
let response = get_data(&client, "some_fake_id".to_string());
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
let response = client.delete("/some_fake_id").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let response = get_data(&client, "some_fake_id".to_string());
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
|
||||
it "can get raw contents" {
|
||||
// store data via post request
|
||||
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
|
||||
|
||||
// retrieve the data via get request
|
||||
let mut response = get_data(&client, format!("raw/{}", id));
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::Plain));
|
||||
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
|
||||
}
|
||||
|
||||
it "can download contents" {
|
||||
// store data via post request
|
||||
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
|
||||
|
||||
// retrieve the data via get request
|
||||
let mut response = get_data(&client, format!("download/{}", id));
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::Binary));
|
||||
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
|
||||
}
|
||||
|
||||
it "can clone contents" {
|
||||
// store data via post request
|
||||
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
|
||||
|
||||
// retrieve the data via get request
|
||||
let mut response = get_data(&client, format!("new?id={}", id));
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
|
||||
}
|
||||
|
||||
it "can't get burned paste" {
|
||||
// store data via post request
|
||||
let id = insert_data(&client, "random_test_data_to_be_checked", "/?burn=true");
|
||||
let response = get_data(&client, id.clone());
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
// retrieve the data via get request
|
||||
let response = get_data(&client, id);
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
|
||||
it "can't get expired paste" {
|
||||
use std::{thread, time};
|
||||
|
||||
// store data via post request
|
||||
let id = insert_data(&client, "random_test_data_to_be_checked", "/?ttl=1");
|
||||
let response = get_data(&client, id.clone());
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
thread::sleep(time::Duration::from_secs(1));
|
||||
|
||||
// retrieve the data via get request
|
||||
let response = get_data(&client, id);
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
|
||||
it "can get static contents" {
|
||||
let mut response = client.get("/static/favicon.ico").dispatch();
|
||||
let contents = std::fs::read("static/favicon.ico").unwrap();
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.body_bytes(), Some(contents));
|
||||
}
|
||||
}
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(
|
||||
name = "pastebin",
|
||||
about = "Simple, standalone and fast pastebin service."
|
||||
)]
|
||||
struct PastebinConfig {
|
||||
#[structopt(
|
||||
long = "address",
|
||||
help = "IP address or host to listen on",
|
||||
default_value = "localhost"
|
||||
)]
|
||||
address: String,
|
||||
|
||||
#[structopt(
|
||||
long = "port",
|
||||
help = "Port number to listen on",
|
||||
default_value = "8000"
|
||||
)]
|
||||
port: u16,
|
||||
|
||||
#[structopt(
|
||||
long = "environment",
|
||||
help = "Rocket server environment",
|
||||
default_value = "production"
|
||||
)]
|
||||
environment: String,
|
||||
|
||||
#[structopt(
|
||||
long = "workers",
|
||||
help = "Number of concurrent thread workers",
|
||||
default_value = "8"
|
||||
)]
|
||||
workers: u16,
|
||||
|
||||
#[structopt(
|
||||
long = "keep-alive",
|
||||
help = "Keep-alive timeout in seconds",
|
||||
default_value = "5"
|
||||
)]
|
||||
keep_alive: u32,
|
||||
|
||||
#[structopt(long = "log", help = "Max log level", default_value = "normal")]
|
||||
log: rocket::config::LoggingLevel,
|
||||
|
||||
#[structopt(
|
||||
long = "ttl",
|
||||
help = "Time to live for entries, by default kept forever",
|
||||
default_value = "0"
|
||||
)]
|
||||
ttl: u64,
|
||||
|
||||
#[structopt(
|
||||
long = "db",
|
||||
help = "Database file path",
|
||||
default_value = "./pastebin.db"
|
||||
)]
|
||||
db_path: String,
|
||||
|
||||
#[structopt(long = "tls-certs", help = "Path to certificate chain in PEM format")]
|
||||
tls_certs: Option<String>,
|
||||
|
||||
#[structopt(
|
||||
long = "tls-key",
|
||||
help = "Path to private key for tls-certs in PEM format"
|
||||
)]
|
||||
tls_key: Option<String>,
|
||||
}
|
||||
|
||||
fn get_url(cfg: &PastebinConfig) -> String {
|
||||
let port = if vec![443, 80].contains(&cfg.port) {
|
||||
String::from("")
|
||||
} else {
|
||||
format!(":{}", cfg.port)
|
||||
};
|
||||
let scheme = if cfg.tls_certs.is_some() {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
};
|
||||
|
||||
format!(
|
||||
"{scheme}://{address}{port}",
|
||||
scheme = scheme,
|
||||
port = port,
|
||||
address = cfg.address,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_error_response(html: String, status: Status, cfg: &PastebinConfig) -> Response {
|
||||
let map = btreemap! {
|
||||
"hostname" => cfg.address.as_str(),
|
||||
"version" => VERSION,
|
||||
"is_error" => "true",
|
||||
};
|
||||
|
||||
let content = Handlebars::new()
|
||||
.render_template(html.as_str(), &map)
|
||||
.unwrap();
|
||||
|
||||
Response::build()
|
||||
.status(status)
|
||||
.sized_body(Cursor::new(content))
|
||||
.finalize()
|
||||
}
|
||||
|
||||
#[post("/?<lang>&<ttl>&<burn>&<encrypted>", data = "<paste>")]
|
||||
fn create(
|
||||
paste: Data,
|
||||
state: State<DB>,
|
||||
cfg: State<PastebinConfig>,
|
||||
lang: Option<String>,
|
||||
ttl: Option<u64>,
|
||||
burn: Option<bool>,
|
||||
encrypted: Option<bool>,
|
||||
) -> Result<String, io::Error> {
|
||||
let id = nanoid!();
|
||||
let url = format!("{url}/{id}", url = get_url(cfg.inner()), id = id);
|
||||
|
||||
let mut writer: Vec<u8> = vec![];
|
||||
new_entry(
|
||||
&mut writer,
|
||||
&mut paste.open(),
|
||||
lang.unwrap_or_else(|| String::from("markup")),
|
||||
ttl.unwrap_or(cfg.ttl),
|
||||
burn.unwrap_or(false),
|
||||
encrypted.unwrap_or(false),
|
||||
);
|
||||
|
||||
state.put(id, writer).unwrap();
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[delete("/<id>")]
|
||||
fn remove(id: String, state: State<DB>) -> Result<(), rocksdb::Error> {
|
||||
state.delete(id)
|
||||
}
|
||||
|
||||
#[get("/<id>?<lang>")]
|
||||
fn get<'r>(
|
||||
id: String,
|
||||
lang: Option<String>,
|
||||
state: State<'r, DB>,
|
||||
resources: State<'r, HashMap<&str, &[u8]>>,
|
||||
cfg: State<PastebinConfig>,
|
||||
) -> Response<'r> {
|
||||
let html = String::from_utf8_lossy(resources.get("../static/index.html").unwrap()).to_string();
|
||||
|
||||
// handle missing entry
|
||||
let root = match get_entry_data(&id, &state) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
let err_kind = match e.kind() {
|
||||
io::ErrorKind::NotFound => Status::NotFound,
|
||||
_ => Status::InternalServerError,
|
||||
};
|
||||
|
||||
let map = btreemap! {
|
||||
"hostname" => cfg.address.as_str(),
|
||||
"version" => VERSION,
|
||||
"is_error" => "true",
|
||||
};
|
||||
|
||||
let content = Handlebars::new()
|
||||
.render_template(html.as_str(), &map)
|
||||
.unwrap();
|
||||
|
||||
return Response::build()
|
||||
.status(err_kind)
|
||||
.sized_body(Cursor::new(content))
|
||||
.finalize();
|
||||
}
|
||||
};
|
||||
|
||||
// handle existing entry
|
||||
let entry = get_root_as_entry(&root);
|
||||
let selected_lang = lang
|
||||
.unwrap_or_else(|| entry.lang().unwrap().to_string())
|
||||
.to_lowercase();
|
||||
|
||||
let mut map = btreemap! {
|
||||
"is_created" => "true".to_string(),
|
||||
"pastebin_code" => std::str::from_utf8(entry.data().unwrap()).unwrap().to_string(),
|
||||
"pastebin_id" => id,
|
||||
"pastebin_language" => selected_lang,
|
||||
"hostname" => cfg.address.clone(),
|
||||
"version" => VERSION.to_string(),
|
||||
};
|
||||
|
||||
if entry.burn() {
|
||||
map.insert(
|
||||
"msg",
|
||||
"FOR YOUR EYES ONLY. The paste is gone, after you close this window.".to_string(),
|
||||
);
|
||||
map.insert("level", "warning".to_string());
|
||||
map.insert("is_burned", "true".to_string());
|
||||
map.insert("glyph", "fa fa-fire".to_string());
|
||||
} else if entry.expiry_timestamp() != 0 {
|
||||
let time = NaiveDateTime::from_timestamp(entry.expiry_timestamp() as i64, 0).format("%Y-%m-%d %H:%M:%S");
|
||||
map.insert("msg", format!("This paste will expire on {}.", time));
|
||||
map.insert("level", "info".to_string());
|
||||
map.insert("glyph", "far fa-clock".to_string());
|
||||
}
|
||||
|
||||
if entry.encrypted() {
|
||||
map.insert("is_encrypted", "true".to_string());
|
||||
}
|
||||
|
||||
let content = Handlebars::new()
|
||||
.render_template(html.as_str(), &map)
|
||||
.unwrap();
|
||||
|
||||
Response::build()
|
||||
.status(Status::Ok)
|
||||
.header(ContentType::HTML)
|
||||
.sized_body(Cursor::new(content))
|
||||
.finalize()
|
||||
}
|
||||
|
||||
#[get("/new?<id>&<level>&<msg>&<glyph>&<url>")]
|
||||
fn get_new<'r>(
|
||||
state: State<'r, DB>,
|
||||
resources: State<'r, HashMap<&str, &[u8]>>,
|
||||
cfg: State<PastebinConfig>,
|
||||
id: Option<String>,
|
||||
level: Option<String>,
|
||||
glyph: Option<String>,
|
||||
msg: Option<String>,
|
||||
url: Option<String>,
|
||||
) -> Response<'r> {
|
||||
let html = String::from_utf8_lossy(resources.get("../static/index.html").unwrap()).to_string();
|
||||
let msg = msg.unwrap_or_else(|| String::from(""));
|
||||
let level = level.unwrap_or_else(|| String::from("secondary"));
|
||||
let glyph = glyph.unwrap_or_else(|| String::from(""));
|
||||
let url = url.unwrap_or_else(|| String::from(""));
|
||||
let root: Vec<u8>;
|
||||
|
||||
let mut map = btreemap! {
|
||||
"is_editable" => "true",
|
||||
"hostname" => cfg.address.as_str(),
|
||||
"version" => VERSION,
|
||||
"msg" => msg.as_str(),
|
||||
"level" => level.as_str(),
|
||||
"glyph" => glyph.as_str(),
|
||||
"url" => url.as_str(),
|
||||
};
|
||||
|
||||
if let Some(id) = id {
|
||||
root = get_entry_data(&id, &state).unwrap();
|
||||
let entry = get_root_as_entry(&root);
|
||||
|
||||
if entry.encrypted() {
|
||||
map.insert("is_encrypted", "true");
|
||||
}
|
||||
|
||||
map.insert(
|
||||
"pastebin_code",
|
||||
std::str::from_utf8(entry.data().unwrap()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let content = Handlebars::new()
|
||||
.render_template(html.as_str(), &map)
|
||||
.unwrap();
|
||||
|
||||
Response::build()
|
||||
.status(Status::Ok)
|
||||
.header(ContentType::HTML)
|
||||
.sized_body(Cursor::new(content))
|
||||
.finalize()
|
||||
}
|
||||
|
||||
#[get("/raw/<id>")]
|
||||
fn get_raw(id: String, state: State<DB>) -> Response {
|
||||
// handle missing entry
|
||||
let root = match get_entry_data(&id, &state) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
let err_kind = match e.kind() {
|
||||
io::ErrorKind::NotFound => Status::NotFound,
|
||||
_ => Status::InternalServerError,
|
||||
};
|
||||
|
||||
return Response::build().status(err_kind).finalize();
|
||||
}
|
||||
};
|
||||
|
||||
let entry = get_root_as_entry(&root);
|
||||
let mut data: Vec<u8> = vec![];
|
||||
|
||||
io::copy(&mut entry.data().unwrap(), &mut data).unwrap();
|
||||
|
||||
Response::build()
|
||||
.status(Status::Ok)
|
||||
.header(ContentType::Plain)
|
||||
.sized_body(Cursor::new(data))
|
||||
.finalize()
|
||||
}
|
||||
|
||||
#[get("/download/<id>")]
|
||||
fn get_binary(id: String, state: State<DB>) -> Response {
|
||||
let response = get_raw(id, state);
|
||||
Response::build_from(response)
|
||||
.header(ContentType::Binary)
|
||||
.finalize()
|
||||
}
|
||||
|
||||
#[get("/static/<resource>")]
|
||||
fn get_static<'r>(
|
||||
resource: String,
|
||||
resources: State<'r, HashMap<&str, &[u8]>>,
|
||||
cfg: State<'r, PastebinConfig>,
|
||||
) -> Response<'r> {
|
||||
let pth = format!("../static/{}", resource);
|
||||
let ext = get_extension(resource.as_str()).replace(".", "");
|
||||
|
||||
let content = match resources.get(pth.as_str()) {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
let html =
|
||||
String::from_utf8_lossy(resources.get("../static/index.html").unwrap()).to_string();
|
||||
return get_error_response(html, Status::NotFound, cfg.inner());
|
||||
}
|
||||
};
|
||||
let content_type = ContentType::from_extension(ext.as_str()).unwrap();
|
||||
|
||||
Response::build()
|
||||
.status(Status::Ok)
|
||||
.header(content_type)
|
||||
.sized_body(Cursor::new(content.iter()))
|
||||
.finalize()
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index() -> Redirect {
|
||||
Redirect::to("/new")
|
||||
}
|
||||
|
||||
fn rocket(pastebin_config: PastebinConfig) -> rocket::Rocket {
|
||||
// parse command line opts
|
||||
let environ: Environment = pastebin_config.environment.parse().unwrap();
|
||||
let mut rocket_config = Config::build(environ)
|
||||
.address(pastebin_config.address.clone())
|
||||
.port(pastebin_config.port)
|
||||
.workers(pastebin_config.workers)
|
||||
.keep_alive(pastebin_config.keep_alive)
|
||||
.log_level(pastebin_config.log)
|
||||
.finalize()
|
||||
.unwrap();
|
||||
|
||||
// handle tls cert setup
|
||||
if pastebin_config.tls_certs.is_some() && pastebin_config.tls_key.is_some() {
|
||||
rocket_config
|
||||
.set_tls(
|
||||
pastebin_config.tls_certs.clone().unwrap().as_str(),
|
||||
pastebin_config.tls_key.clone().unwrap().as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// setup db
|
||||
let db = DB::open_default(pastebin_config.db_path.clone()).unwrap();
|
||||
let mut db_opts = Options::default();
|
||||
|
||||
db_opts.create_if_missing(true);
|
||||
db_opts.set_compaction_filter("ttl_entries", compaction_filter_expired_entries);
|
||||
|
||||
let resources = load_static_resources!(
|
||||
"../static/index.html",
|
||||
"../static/custom.js",
|
||||
"../static/custom.css",
|
||||
"../static/prism.js",
|
||||
"../static/prism.css",
|
||||
"../static/favicon.ico"
|
||||
);
|
||||
|
||||
// run rocket
|
||||
rocket::custom(rocket_config)
|
||||
.manage(pastebin_config)
|
||||
.manage(db)
|
||||
.manage(resources)
|
||||
.mount(
|
||||
"/",
|
||||
routes![index, create, remove, get, get_new, get_raw, get_binary, get_static],
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let pastebin_config = PastebinConfig::from_args();
|
||||
rocket(pastebin_config).launch();
|
||||
}
|
28
static/custom.css
Normal file
28
static/custom.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 75rem; /* Minimum navbar */
|
||||
padding-top: 3.5rem; /* Margin top by nav bar */
|
||||
padding-bottom: 3.5rem; /* Margin bottom by footer height */
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px; /* Set the fixed height of the footer here */
|
||||
line-height: 60px; /* Vertically center the text there */
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
margin: 0.5em 0px;
|
||||
}
|
||||
|
||||
pre[class*="language-"]:before,
|
||||
pre[class*="language-"]:after {
|
||||
display: none;
|
||||
}
|
156
static/custom.js
Normal file
156
static/custom.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
$(document).ready(function() {
|
||||
function replaceUrlParam(url, param, value) {
|
||||
if (value == null) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
var pattern = new RegExp('\\b('+param+'=).*?(&|#|$)');
|
||||
if (url.search(pattern)>=0) {
|
||||
return url.replace(pattern,'$1' + value + '$2');
|
||||
}
|
||||
|
||||
url = url.replace(/[?#]$/,'');
|
||||
return url + (url.indexOf('?')>0 ? '&' : '?') + param + '=' + value;
|
||||
}
|
||||
|
||||
function resetLanguageSelector() {
|
||||
var url = new URL(document.location);
|
||||
var params = url.searchParams;
|
||||
var lang = params.get("lang");
|
||||
|
||||
if (lang != null) {
|
||||
$("#language-selector").val(lang);
|
||||
} else {
|
||||
if($("#pastebin-code-block").length) {
|
||||
$("#language-selector").val(
|
||||
$("#pastebin-code-block").prop("class").trim().split('-')[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkPasswordModal() {
|
||||
if ($("#password-modal").length) {
|
||||
$('#password-modal').modal('toggle');
|
||||
}
|
||||
}
|
||||
|
||||
var state = {
|
||||
expiry: 0,
|
||||
burn: 0,
|
||||
};
|
||||
|
||||
resetLanguageSelector();
|
||||
checkPasswordModal();
|
||||
|
||||
$("#language-selector").change(function() {
|
||||
if ($("#pastebin-code-block").length) {
|
||||
$('#pastebin-code-block').attr('class', 'language-' + $("#language-selector").val());
|
||||
Prism.highlightElement($('#pastebin-code-block')[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$("#remove-btn").on("click", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
url: window.location.pathname,
|
||||
type: 'DELETE',
|
||||
success: function(result) {
|
||||
uri = "/new";
|
||||
uri = replaceUrlParam(uri, 'level', "info");
|
||||
uri = replaceUrlParam(uri, 'glyph', "fas fa-info-circle");
|
||||
uri = replaceUrlParam(uri, 'msg', "The paste has been successfully removed.");
|
||||
window.location.href = encodeURI(uri);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#copy-btn").on("click", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$(".toolbar-item button").get(0).click();
|
||||
|
||||
var $this = $(this);
|
||||
$this.text("Copied!");
|
||||
$this.attr("disabled", "disabled");
|
||||
|
||||
setTimeout(function() {
|
||||
$this.text("Copy");
|
||||
$this.removeAttr("disabled");
|
||||
}, 800);
|
||||
|
||||
});
|
||||
|
||||
$("#send-btn").on("click", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
uri = "/";
|
||||
uri = replaceUrlParam(uri, 'lang', $("#language-selector").val());
|
||||
uri = replaceUrlParam(uri, 'ttl', state.expiry);
|
||||
uri = replaceUrlParam(uri, 'burn', state.burn);
|
||||
|
||||
var data = $("#content-textarea").val();
|
||||
var pass = $("#pastebin-password").val();
|
||||
|
||||
if ($("#pastebin-password").val().length > 0) {
|
||||
data = CryptoJS.AES.encrypt(data, pass).toString();
|
||||
uri = replaceUrlParam(uri, 'encrypted', true);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: uri,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
success: function(result) {
|
||||
uri = "/new";
|
||||
uri = replaceUrlParam(uri, 'level', "success");
|
||||
uri = replaceUrlParam(uri, 'glyph', "fas fa-check");
|
||||
uri = replaceUrlParam(uri, 'msg', "The paste has been successfully created:");
|
||||
uri = replaceUrlParam(uri, 'url', result);
|
||||
|
||||
window.location.href = encodeURI(uri);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#expiry-dropdown a').click(function(event){
|
||||
event.preventDefault();
|
||||
|
||||
state.expiry = $(this).attr("href");
|
||||
$('#expiry-dropdown-btn').text("Expires: " + this.innerHTML);
|
||||
});
|
||||
|
||||
$('#burn-dropdown a').click(function(event){
|
||||
event.preventDefault();
|
||||
|
||||
state.burn = $(this).attr("href");
|
||||
$('#burn-dropdown-btn').text("Burn: " + this.innerHTML);
|
||||
});
|
||||
|
||||
$('#decrypt-btn').click(function(event) {
|
||||
var pass = $("#modal-password").val();
|
||||
var data = "";
|
||||
|
||||
if ($("#pastebin-code-block").length) {
|
||||
data = $("#pastebin-code-block").text();
|
||||
} else {
|
||||
data = $("#content-textarea").text();
|
||||
}
|
||||
|
||||
var decrypted = CryptoJS.AES.decrypt(data, pass).toString(CryptoJS.enc.Utf8);
|
||||
if (decrypted.length == 0) {
|
||||
$("#modal-alert").removeClass("collapse");
|
||||
} else {
|
||||
if ($("#pastebin-code-block").length) {
|
||||
$("#pastebin-code-block").text(decrypted);
|
||||
Prism.highlightElement($('#pastebin-code-block')[0]);
|
||||
} else {
|
||||
$("#content-textarea").text(decrypted);
|
||||
}
|
||||
|
||||
$("#modal-close-btn").click();
|
||||
$("#modal-alert").alert('close');
|
||||
}
|
||||
});
|
||||
});
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
319
static/index.html
Normal file
319
static/index.html
Normal file
|
@ -0,0 +1,319 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="Simple, fast and standalone pastebin service">
|
||||
<meta name="author" content="Mateusz Kaczanowski">
|
||||
|
||||
<title>Pastebin</title>
|
||||
|
||||
<link rel="icon" href="/static/favicon.ico">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
|
||||
|
||||
<link href="/static/prism.css" rel="stylesheet" />
|
||||
<link href="/static/custom.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<a class="navbar-brand" href="/">PASTEBIN</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/new">New <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
{{#if is_created}}
|
||||
{{#if (not is_burned)}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" id="remove-btn">Remove</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/new?id={{pastebin_id}}" id="clone-btn">Clone</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if is_editable}}
|
||||
<li class="nav-item dropdown">
|
||||
<a id="expiry-dropdown-btn" class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Expires: Never</a>
|
||||
<div class="dropdown-menu" id="expiry-dropdown">
|
||||
<a class="dropdown-item" href="300">5 minutes</a>
|
||||
<a class="dropdown-item" href="600">10 minutes</a>
|
||||
<a class="dropdown-item" href="3600">1 hour</a>
|
||||
<a class="dropdown-item" href="86400">1 day</a>
|
||||
<a class="dropdown-item" href="604800">1 week</a>
|
||||
<a class="dropdown-item" href="2419200">1 month</a>
|
||||
<a class="dropdown-item" href="29030400">1 year</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="0">Never</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a id="burn-dropdown-btn" class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Burn: No</a>
|
||||
<div class="dropdown-menu" id="burn-dropdown">
|
||||
<a class="dropdown-item" href="true">Yes</a>
|
||||
<a class="dropdown-item" href="false">No</a>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
||||
<div class="form-inline mt-2 mt-md-0">
|
||||
{{#if is_editable}}
|
||||
<input class="form-control mr-sm-2" id="pastebin-password" type="password" placeholder="Password (optional)" aria-label="Password (optional)">
|
||||
{{/if}}
|
||||
<select class="form-control mr-sm-2" id="language-selector">
|
||||
<option value="markup">Markup</option>
|
||||
<option value="css">CSS</option>
|
||||
<option value="clike">C-like</option>
|
||||
<option value="javascript">JavaScript</option>
|
||||
<option disabled>──────────</option>
|
||||
<option value="abap">ABAP</option>
|
||||
<option value="actionscript">ActionScript</option>
|
||||
<option value="ada">Ada</option>
|
||||
<option value="apacheconf">Apache Configuration</option>
|
||||
<option value="apl">APL</option>
|
||||
<option value="applescript">AppleScript</option>
|
||||
<option value="arduino">Arduino</option>
|
||||
<option value="arff">ARFF</option>
|
||||
<option value="asciidoc">AsciiDoc</option>
|
||||
<option value="asm6502">6502 Assembly</option>
|
||||
<option value="aspnet">ASP.NET (C#)</option>
|
||||
<option value="autohotkey">AutoHotkey</option>
|
||||
<option value="autoit">AutoIt</option>
|
||||
<option value="bash">Bash</option>
|
||||
<option value="basic">BASIC</option>
|
||||
<option value="batch">Batch</option>
|
||||
<option value="bison">Bison</option>
|
||||
<option value="brainfuck">Brainfuck</option>
|
||||
<option value="bro">Bro</option>
|
||||
<option value="c">C</option>
|
||||
<option value="csharp">C#</option>
|
||||
<option value="cpp">C++</option>
|
||||
<option value="coffeescript">CoffeeScript</option>
|
||||
<option value="clojure">Clojure</option>
|
||||
<option value="crystal">Crystal</option>
|
||||
<option value="csp">Content-Security-Policy</option>
|
||||
<option value="css-extras">CSS Extras</option>
|
||||
<option value="d">D</option>
|
||||
<option value="dart">Dart</option>
|
||||
<option value="diff">Diff</option>
|
||||
<option value="django">Django/Jinja2</option>
|
||||
<option value="docker">Docker</option>
|
||||
<option value="eiffel">Eiffel</option>
|
||||
<option value="elixir">Elixir</option>
|
||||
<option value="elm">Elm</option>
|
||||
<option value="erb">ERB</option>
|
||||
<option value="erlang">Erlang</option>
|
||||
<option value="fsharp">F#</option>
|
||||
<option value="flow">Flow</option>
|
||||
<option value="fortran">Fortran</option>
|
||||
<option value="gedcom">GEDCOM</option>
|
||||
<option value="gherkin">Gherkin</option>
|
||||
<option value="git">Git</option>
|
||||
<option value="glsl">GLSL</option>
|
||||
<option value="gml">GameMaker</option>
|
||||
<option value="go">Go</option>
|
||||
<option value="graphql">GraphQL</option>
|
||||
<option value="groovy">Groovy</option>
|
||||
<option value="haml">Haml</option>
|
||||
<option value="handlebars">Handlebars</option>
|
||||
<option value="haskell">Haskell</option>
|
||||
<option value="haxe">Haxe</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="hpkp">HTTP Public-Key-Pins</option>
|
||||
<option value="hsts">HTTP STS</option>
|
||||
<option value="ichigojam">IchigoJam</option>
|
||||
<option value="icon">Icon</option>
|
||||
<option value="inform7">Inform 7</option>
|
||||
<option value="ini">Ini</option>
|
||||
<option value="io">Io</option>
|
||||
<option value="j">J</option>
|
||||
<option value="java">Java</option>
|
||||
<option value="jolie">Jolie</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="julia">Julia</option>
|
||||
<option value="keyman">Keyman</option>
|
||||
<option value="kotlin">Kotlin</option>
|
||||
<option value="latex">LaTeX</option>
|
||||
<option value="less">Less</option>
|
||||
<option value="liquid">Liquid</option>
|
||||
<option value="lisp">Lisp</option>
|
||||
<option value="livescript">LiveScript</option>
|
||||
<option value="lolcode">LOLCODE</option>
|
||||
<option value="lua">Lua</option>
|
||||
<option value="makefile">Makefile</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="markup-templating">Markup templating</option>
|
||||
<option value="matlab">MATLAB</option>
|
||||
<option value="mel">MEL</option>
|
||||
<option value="mizar">Mizar</option>
|
||||
<option value="monkey">Monkey</option>
|
||||
<option value="n4js">N4JS</option>
|
||||
<option value="nasm">NASM</option>
|
||||
<option value="nginx">nginx</option>
|
||||
<option value="nim">Nim</option>
|
||||
<option value="nix">Nix</option>
|
||||
<option value="nsis">NSIS</option>
|
||||
<option value="objectivec">Objective-C</option>
|
||||
<option value="ocaml">OCaml</option>
|
||||
<option value="opencl">OpenCL</option>
|
||||
<option value="oz">Oz</option>
|
||||
<option value="parigp">PARI/GP</option>
|
||||
<option value="parser">Parser</option>
|
||||
<option value="pascal">Pascal</option>
|
||||
<option value="perl">Perl</option>
|
||||
<option value="php">PHP</option>
|
||||
<option value="php-extras">PHP Extras</option>
|
||||
<option value="plsql">PL/SQL</option>
|
||||
<option value="powershell">PowerShell</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="prolog">Prolog</option>
|
||||
<option value="properties">.properties</option>
|
||||
<option value="protobuf">Protocol Buffers</option>
|
||||
<option value="pug">Pug</option>
|
||||
<option value="puppet">Puppet</option>
|
||||
<option value="pure">Pure</option>
|
||||
<option value="python">Python</option>
|
||||
<option value="q">Q (kdb+ database)</option>
|
||||
<option value="qore">Qore</option>
|
||||
<option value="r">R</option>
|
||||
<option value="jsx">React JSX</option>
|
||||
<option value="tsx">React TSX</option>
|
||||
<option value="renpy">Ren'py</option>
|
||||
<option value="reason">Reason</option>
|
||||
<option value="rest">reST (reStructuredText)</option>
|
||||
<option value="rip">Rip</option>
|
||||
<option value="roboconf">Roboconf</option>
|
||||
<option value="ruby">Ruby</option>
|
||||
<option value="rust">Rust</option>
|
||||
<option value="sas">SAS</option>
|
||||
<option value="sass">Sass (Sass)</option>
|
||||
<option value="scss">Sass (Scss)</option>
|
||||
<option value="scala">Scala</option>
|
||||
<option value="scheme">Scheme</option>
|
||||
<option value="smalltalk">Smalltalk</option>
|
||||
<option value="smarty">Smarty</option>
|
||||
<option value="sql">SQL</option>
|
||||
<option value="soy">Soy (Closure Template)</option>
|
||||
<option value="stylus">Stylus</option>
|
||||
<option value="swift">Swift</option>
|
||||
<option value="tap">TAP</option>
|
||||
<option value="tcl">Tcl</option>
|
||||
<option value="textile">Textile</option>
|
||||
<option value="tt2">Template Toolkit 2</option>
|
||||
<option value="twig">Twig</option>
|
||||
<option value="typescript">TypeScript</option>
|
||||
<option value="vbnet">VB.Net</option>
|
||||
<option value="velocity">Velocity</option>
|
||||
<option value="verilog">Verilog</option>
|
||||
<option value="vhdl">VHDL</option>
|
||||
<option value="vim">vim</option>
|
||||
<option value="visual-basic">Visual Basic</option>
|
||||
<option value="wasm">WebAssembly</option>
|
||||
<option value="wiki">Wiki markup</option>
|
||||
<option value="xeora">Xeora</option>
|
||||
<option value="xojo">Xojo (REALbasic)</option>
|
||||
<option value="xquery">XQuery</option>
|
||||
<option value="yaml">YAML</option>
|
||||
</select>
|
||||
{{#if is_created}}
|
||||
<a id="copy-btn" class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0" href="#">Copy</a>
|
||||
{{#if (not is_burned)}}
|
||||
<a class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0 ml-md-2" href="/raw/{{pastebin_id}}">Raw</a>
|
||||
<a class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0 ml-md-2" href="/download/{{pastebin_id}}">Download</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if is_editable}}
|
||||
<a id="send-btn" class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0" href="#">Send</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main role="main" class="container-fluid">
|
||||
{{# if is_error}}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-12 text-center">
|
||||
<span class="display-1 d-block">404</span>
|
||||
<div class="mb-4 lead">The page you are looking for was not found.</div>
|
||||
<a href="/" class="btn btn-link">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{#if msg}}
|
||||
<div class="mt-3 alert alert-{{level}} alert-dismissible fade show" role="alert">
|
||||
{{#if glyph}}<i class="{{glyph}}"></i>{{/if}}
|
||||
{{#if url}}{{msg}} <a href="{{url}}">{{url}}</a>{{else}}{{msg}}{{/if}}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{# if is_editable}}
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="content-textarea" rows="25">{{pastebin_code}}</textarea>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if is_created or is_clone}}
|
||||
<pre><code id="pastebin-code-block" class="language-{{pastebin_language}}">{{pastebin_code}}</code></pre>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<span class="text-muted ml-4">{{hostname}} - pastebin v{{version}}</span>
|
||||
</footer>
|
||||
|
||||
{{#if is_encrypted}}
|
||||
<div class="modal fade" id="password-modal" tabindex="-1" role="dialog" aria-labelledby="password-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="password-modal-label">
|
||||
<i class="fas fa-key"></i>
|
||||
Please enter the password for this paste
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="alert alert-danger collapse" role="alert" id="modal-alert">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Could not decrypt data. Wrong password?
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="modal-password">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modal-close-btn">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="decrypt-btn">Decrypt</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-notify/0.2.0/js/bootstrap-notify.min.js"></script>
|
||||
<script src="/static/prism.js"></script>
|
||||
<script src="/static/custom.js"></script>
|
||||
</body>
|
||||
</html>
|
358
static/prism.css
Normal file
358
static/prism.css
Normal file
|
@ -0,0 +1,358 @@
|
|||
/* PrismJS 1.20.0
|
||||
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+al+antlr4+apacheconf+apl+applescript+aql+arduino+arff+asciidoc+asm6502+aspnet+autohotkey+autoit+bash+basic+batch+bbcode+bison+bnf+brainfuck+brightscript+bro+c+concurnas+csharp+cpp+cil+coffeescript+cmake+clojure+crystal+csp+css-extras+d+dart+dax+diff+django+dns-zone-file+docker+ebnf+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+firestore-security-rules+flow+fortran+ftl+gcode+gdscript+gedcom+gherkin+git+glsl+gml+go+graphql+groovy+haml+handlebars+haskell+haxe+hcl+http+hpkp+hsts+ichigojam+icon+iecst+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jolie+jq+jsdoc+js-extras+js-templates+json+jsonp+json5+julia+keyman+kotlin+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+lolcode+lua+makefile+markdown+markup-templating+matlab+mel+mizar+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+nasm+neon+nginx+nim+nix+nsis+objectivec+ocaml+opencl+oz+parigp+parser+pascal+pascaligo+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+processing+prolog+properties+protobuf+pug+puppet+pure+python+q+qml+qore+r+racket+jsx+tsx+renpy+reason+regex+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smalltalk+smarty+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+stylus+swift+tap+tcl+textile+toml+tt2+turtle+twig+typescript+t4-cs+t4-vb+t4-templating+unrealscript+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+wiki+xeora+xojo+xquery+yaml+zig&plugins=line-highlight+remove-initial-line-feed+toolbar+copy-to-clipboard+diff-highlight */
|
||||
/**
|
||||
* prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML
|
||||
* Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics);
|
||||
* @author Tim Shedor
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
position: relative;
|
||||
margin: .5em 0;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
pre[class*="language-"]>code {
|
||||
position: relative;
|
||||
border-left: 10px solid #358ccb;
|
||||
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
|
||||
background-color: #fdfdfd;
|
||||
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
|
||||
background-size: 3em 3em;
|
||||
background-origin: content-box;
|
||||
background-attachment: local;
|
||||
}
|
||||
|
||||
code[class*="language"] {
|
||||
max-height: inherit;
|
||||
height: inherit;
|
||||
padding: 0 1em;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Margin bottom to accommodate shadow */
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background-color: #fdfdfd;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
position: relative;
|
||||
padding: .2em;
|
||||
border-radius: 0.3em;
|
||||
color: #c92c2c;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
display: inline;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
pre[class*="language-"]:before,
|
||||
pre[class*="language-"]:after {
|
||||
content: '';
|
||||
z-index: -2;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0.75em;
|
||||
left: 0.18em;
|
||||
width: 40%;
|
||||
height: 20%;
|
||||
max-height: 13em;
|
||||
box-shadow: 0px 13px 8px #979797;
|
||||
-webkit-transform: rotate(-2deg);
|
||||
-moz-transform: rotate(-2deg);
|
||||
-ms-transform: rotate(-2deg);
|
||||
-o-transform: rotate(-2deg);
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"]:after,
|
||||
pre[class*="language-"]:after {
|
||||
right: 0.75em;
|
||||
left: auto;
|
||||
-webkit-transform: rotate(2deg);
|
||||
-moz-transform: rotate(2deg);
|
||||
-ms-transform: rotate(2deg);
|
||||
-o-transform: rotate(2deg);
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #7D8B99;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #5F6364;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function-name,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #c92c2c;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.function,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #2f9c0a;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.token.variable {
|
||||
color: #a67f59;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword,
|
||||
.token.class-name {
|
||||
color: #1990b8;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #a67f59;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.token.important {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
pre[class*="language-"]:before,
|
||||
pre[class*="language-"]:after {
|
||||
bottom: 14px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Plugin styles */
|
||||
.token.tab:not(:empty):before,
|
||||
.token.cr:before,
|
||||
.token.lf:before {
|
||||
color: #e0d7d1;
|
||||
}
|
||||
|
||||
/* Plugin styles: Line Numbers */
|
||||
pre[class*="language-"].line-numbers.line-numbers {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
pre[class*="language-"].line-numbers.line-numbers code {
|
||||
padding-left: 3.8em;
|
||||
}
|
||||
|
||||
pre[class*="language-"].line-numbers.line-numbers .line-numbers-rows {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Plugin styles: Line Highlight */
|
||||
pre[class*="language-"][data-line] {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
pre[data-line] code {
|
||||
position: relative;
|
||||
padding-left: 4em;
|
||||
}
|
||||
pre .line-highlight {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
pre[data-line] {
|
||||
position: relative;
|
||||
padding: 1em 0 1em 3em;
|
||||
}
|
||||
|
||||
.line-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: inherit 0;
|
||||
margin-top: 1em; /* Same as .prism’s padding-top */
|
||||
|
||||
background: hsla(24, 20%, 50%,.08);
|
||||
background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
line-height: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.line-highlight:before,
|
||||
.line-highlight[data-end]:after {
|
||||
content: attr(data-start);
|
||||
position: absolute;
|
||||
top: .4em;
|
||||
left: .6em;
|
||||
min-width: 1em;
|
||||
padding: 0 .5em;
|
||||
background-color: hsla(24, 20%, 50%,.4);
|
||||
color: hsl(24, 20%, 95%);
|
||||
font: bold 65%/1.5 sans-serif;
|
||||
text-align: center;
|
||||
vertical-align: .3em;
|
||||
border-radius: 999px;
|
||||
text-shadow: none;
|
||||
box-shadow: 0 1px white;
|
||||
}
|
||||
|
||||
.line-highlight[data-end]:after {
|
||||
content: attr(data-end);
|
||||
top: auto;
|
||||
bottom: .4em;
|
||||
}
|
||||
|
||||
.line-numbers .line-highlight:before,
|
||||
.line-numbers .line-highlight:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
div.code-toolbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar {
|
||||
position: absolute;
|
||||
top: .3em;
|
||||
right: .2em;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.code-toolbar:hover > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Separate line b/c rules are thrown out if selector is invalid.
|
||||
IE11 and old Edge versions don't support :focus-within. */
|
||||
div.code-toolbar:focus-within > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar .toolbar-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar button {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
-webkit-user-select: none; /* for button */
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar a,
|
||||
div.code-toolbar > .toolbar button,
|
||||
div.code-toolbar > .toolbar span {
|
||||
color: #bbb;
|
||||
font-size: .8em;
|
||||
padding: 0 .5em;
|
||||
background: #f5f2f0;
|
||||
background: rgba(224, 224, 224, 0.2);
|
||||
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar a:hover,
|
||||
div.code-toolbar > .toolbar a:focus,
|
||||
div.code-toolbar > .toolbar button:hover,
|
||||
div.code-toolbar > .toolbar button:focus,
|
||||
div.code-toolbar > .toolbar span:hover,
|
||||
div.code-toolbar > .toolbar span:focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.deleted:not(.prefix),
|
||||
pre > code.diff-highlight .token.deleted:not(.prefix) {
|
||||
background-color: rgba(255, 0, 0, .1);
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.inserted:not(.prefix),
|
||||
pre > code.diff-highlight .token.inserted:not(.prefix) {
|
||||
background-color: rgba(0, 255, 128, .1);
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
223
static/prism.js
Normal file
223
static/prism.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue