├── t ├── mojopaste.conf ├── safe-params.t ├── config.t ├── nonexisting.t ├── unicode.t ├── Helper.pm ├── branding.t ├── 00-basic.t ├── charts.t └── mojopaste.t ├── .gitignore ├── .travis.yml ├── cpanfile ├── .ship.conf ├── .perltidyrc ├── .github └── workflows │ ├── linux.yml │ └── docker.yml ├── MANIFEST.SKIP ├── Dockerfile ├── Makefile.PL ├── README.md ├── lib └── App │ └── mojopaste.pm ├── Changes └── script └── mojopaste /t/mojopaste.conf: -------------------------------------------------------------------------------- 1 | { 2 | paste_dir => 't/paste', 3 | hypnotoad => { 4 | listen => ['http://*:8080'], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /t/safe-params.t: -------------------------------------------------------------------------------- 1 | use lib '.'; 2 | use t::Helper; 3 | 4 | my $t = t::Helper->t; 5 | $t->get_ok("/?edit=../../Makefile.PL")->status_is(404)->content_unlike(qr{use ExtUtils::MakeMaker;}); 6 | 7 | done_testing; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /META.yml 2 | /MYMETA.* 3 | /blib/ 4 | /inc/ 5 | /pm_to_blib 6 | /MANIFEST 7 | /MANIFEST.bak 8 | /Makefile 9 | /Makefile.old 10 | /local 11 | /paste 12 | *.old 13 | *.swp 14 | ~$ 15 | /App-mojopaste*tar.gz 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - "5.26" 4 | - "5.20" 5 | - "5.16" 6 | install: 7 | - "cpanm -n Test::Pod Test::Pod::Coverage" 8 | - "cpanm -n --installdeps ." 9 | notifications: 10 | email: false 11 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | # You can install this projct with curl -L http://cpanmin.us | perl - https://github.com/jhthorsen/app-mojopaste/archive/master.tar.gz 2 | requires "Mojolicious" => "9.11"; 3 | requires "Text::CSV" => "1.30"; 4 | 5 | test_requires "Test::More" => "0.88"; 6 | -------------------------------------------------------------------------------- /t/config.t: -------------------------------------------------------------------------------- 1 | use lib '.'; 2 | use t::Helper; 3 | 4 | $ENV{MOJO_CONFIG} = Cwd::abs_path('t/mojopaste.conf'); 5 | plan skip_all => 'Cannot read MOJO_CONFIG' unless -r $ENV{MOJO_CONFIG}; 6 | is(t::Helper->t->app->config('paste_dir'), 't/paste', 'read config'); 7 | 8 | done_testing; 9 | -------------------------------------------------------------------------------- /.ship.conf: -------------------------------------------------------------------------------- 1 | # Generated by git-ship. See 'git-ship --man' for help or https://github.com/jhthorsen/app-git-ship 2 | class = App::git::ship::perl 3 | project_name = 4 | homepage = https://github.com/jhthorsen/app-mojopaste 5 | bugtracker = https://github.com/jhthorsen/app-mojopaste/issues 6 | license = artistic_2 7 | build_test_options = # Example: -l -j8 8 | -------------------------------------------------------------------------------- /t/nonexisting.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{PASTE_ENABLE_CHARTS} = 1 } 2 | use lib '.'; 3 | use t::Helper; 4 | 5 | my $t = t::Helper->t; 6 | my $id = substr Mojo::Util::md5_sum('nope'), 0, 12; 7 | 8 | for my $p ("/$id", "/?edit=$id", "/$id/chart") { 9 | $t->get_ok($p)->status_is(404)->text_is('title', 'Could not find paste')->content_like(qr{Could not find paste}); 10 | } 11 | 12 | done_testing; 13 | -------------------------------------------------------------------------------- /.perltidyrc: -------------------------------------------------------------------------------- 1 | -pbp # Start with Perl Best Practices 2 | -w # Show all warnings 3 | -iob # Ignore old breakpoints 4 | -l=120 # 120 characters per line 5 | -mbl=2 # No more than 2 blank lines 6 | -i=2 # Indentation is 2 columns 7 | -ci=2 # Continuation indentation is 2 columns 8 | -vt=0 # Less vertical tightness 9 | -pt=2 # High parenthesis tightness 10 | -bt=2 # High brace tightness 11 | -sbt=2 # High square bracket tightness 12 | -isbc # Don't indent comments without leading space 13 | -wn # Opening and closing containers to be "welded" together 14 | -------------------------------------------------------------------------------- /t/unicode.t: -------------------------------------------------------------------------------- 1 | use lib '.'; 2 | use t::Helper; 3 | 4 | my $t = t::Helper->t; 5 | my $raw = "BLACK DOWN-POINTING TRIANGLE \x{3a3}"; 6 | 7 | plan skip_all => "$ENV{PASTE_DIR} was not created" unless -d $ENV{PASTE_DIR}; 8 | 9 | $t->post_ok('/', form => {paste => $raw, p => 1})->status_is(302); 10 | my ($id) = $t->tx->res->headers->location =~ m!/(\w+)$!; 11 | $raw =~ s/\x{3a3}/Σ/; 12 | $t->get_ok($t->tx->res->headers->location)->text_is('pre', $raw); 13 | $t->get_ok("/$id?raw=1")->content_is($raw); 14 | 15 | require File::Path; 16 | File::Path::remove_tree($ENV{PASTE_DIR}, {keep_root => 1}); 17 | done_testing; 18 | -------------------------------------------------------------------------------- /t/Helper.pm: -------------------------------------------------------------------------------- 1 | package t::Helper; 2 | use Mojo::Base -strict; 3 | use Cwd (); 4 | use File::Spec; 5 | use Test::More (); 6 | 7 | sub t { 8 | Test::More::plan(skip_all => $@) unless do File::Spec->catfile(qw(script mojopaste)); 9 | Test::More::plan(skip_all => "$ENV{PASTE_DIR} was not created") unless -d $ENV{PASTE_DIR}; 10 | return Test::Mojo->new; 11 | } 12 | 13 | sub import { 14 | my $caller = caller; 15 | 16 | $_->import for qw(strict warnings utf8); 17 | $ENV{PASTE_DIR} = File::Spec->catdir(qw(t paste)); 18 | 19 | eval <<"HERE" or die $@; 20 | package $caller; 21 | use Test::Mojo; 22 | use Test::More; 23 | 1; 24 | HERE 25 | } 26 | 1; 27 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - '**' 7 | jobs: 8 | nodejs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | perl: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | perl-version: 17 | - '5.26' 18 | - '5.34' 19 | container: 20 | image: perl:${{matrix.perl-version}} 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: perl -V 24 | run: perl -V 25 | - name: Install dependencies 26 | run: | 27 | cpanm -n Test::Pod Test::Pod::Coverage 28 | cpanm -n --installdeps . 29 | - name: Run perl tests 30 | run: prove -l 31 | env: 32 | HARNESS_OPTIONS: j4 33 | TEST_POD: 1 34 | -------------------------------------------------------------------------------- /t/branding.t: -------------------------------------------------------------------------------- 1 | use lib '.'; 2 | use t::Helper; 3 | 4 | my $t = t::Helper->t; 5 | $t->get_ok('/')->status_is(200)->text_is('title', 'Create new paste - Mojopaste') 6 | ->element_exists('nav .brand[href="/"]')->element_exists('nav .brand img[src="/images/logo.png"]') 7 | ->text_is('nav .brand span', 'Mojopaste'); 8 | 9 | $t->app->defaults(brand_link => 'https://example.com'); 10 | $t->app->defaults(brand_logo => 'https://example.com/logo/mybrand.png'); 11 | $t->app->defaults(brand_name => 'Example'); 12 | $t->get_ok('/')->status_is(200)->text_is('title', 'Create new paste - Example') 13 | ->element_exists('nav .brand[href="https://example.com"]') 14 | ->element_exists('nav .brand img[src="https://example.com/logo/mybrand.png"]')->text_is('nav .brand span', 'Example'); 15 | 16 | $t->app->defaults(brand_link => 'https://example.com'); 17 | $t->app->defaults(brand_logo => ''); 18 | $t->app->defaults(brand_name => ''); 19 | $t->get_ok('/')->status_is(200)->text_is('title', 'Create new paste - Mojopaste') 20 | ->element_exists('nav .brand[href="https://example.com"]')->element_exists_not('nav .brand img') 21 | ->element_exists_not('nav .brand span'); 22 | 23 | done_testing; 24 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | \bRCS\b 2 | \bCVS\b 3 | \bSCCS\b 4 | ,v$ 5 | \B\.svn\b 6 | \B\.git\b 7 | \B\.gitignore\b 8 | \b_darcs\b 9 | \B\.cvsignore$ 10 | 11 | # Avoid VMS specific MakeMaker generated files 12 | \bDescrip.MMS$ 13 | \bDESCRIP.MMS$ 14 | \bdescrip.mms$ 15 | 16 | # Avoid Makemaker generated and utility files. 17 | \bMANIFEST\.bak 18 | \bMakefile$ 19 | \bblib/ 20 | \bMakeMaker-\d 21 | \bpm_to_blib\.ts$ 22 | \bpm_to_blib$ 23 | \bblibdirs\.ts$ # 6.18 through 6.25 generated this 24 | 25 | # Avoid Module::Build generated and utility files. 26 | \bBuild$ 27 | \b_build/ 28 | \bBuild.bat$ 29 | \bBuild.COM$ 30 | \bBUILD.COM$ 31 | \bbuild.com$ 32 | 33 | # Avoid temp and backup files. 34 | ~$ 35 | \.old$ 36 | \#$ 37 | \b\.# 38 | \.bak$ 39 | \.tmp$ 40 | \.# 41 | \.rej$ 42 | 43 | # Avoid OS-specific files/dirs 44 | # Mac OSX metadata 45 | \B\.DS_Store 46 | # Mac OSX SMB mount metadata files 47 | \B\._ 48 | 49 | # Avoid Devel::Cover and Devel::CoverX::Covered files. 50 | \bcover_db\b 51 | \bcovered\b 52 | 53 | # Avoid MYMETA files 54 | ^MYMETA\. 55 | #!end included /home/jhthorsen/.perlbrew/perls/perl-5.18.2/lib/5.18.2/ExtUtils/MANIFEST.SKIP 56 | 57 | \.swp$ 58 | ^local/ 59 | ^paste/ 60 | ^MANIFEST\.SKIP 61 | ^README\.pod 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install docker https://docs.docker.com/engine/installation/linux/ubuntulinux/ 2 | # git clone https://github.com/jhthorsen/app-mojopaste 3 | # cd app-mojopaste 4 | # docker build --no-cache -t mojopaste . 5 | # mkdir /some/dir/fordata 6 | # docker run -d --restart always --name mojopaste -v /some/dir/fordata:/app/data -p 5555:8080 mojopaste 7 | # http://localhost:5555 8 | 9 | FROM alpine:3.5 10 | MAINTAINER jhthorsen@cpan.org 11 | 12 | RUN mkdir -p /app/data 13 | 14 | RUN apk add --no-cache curl openssl perl perl-io-socket-ssl perl-net-ssleay wget \ 15 | && apk add --no-cache --virtual builddeps build-base perl-dev \ 16 | && curl -L https://github.com/jhthorsen/app-mojopaste/archive/main.tar.gz | tar xvz \ 17 | && curl -L https://cpanmin.us | perl - App::cpanminus \ 18 | && cpanm -M https://cpan.metacpan.org Text::CSV \ 19 | && cpanm -M https://cpan.metacpan.org --installdeps ./app-mojopaste-main \ 20 | && apk del builddeps curl \ 21 | && rm -rf /root/.cpanm /var/cache/apk/* 22 | 23 | ENV MOJO_MODE production 24 | ENV PASTE_DIR /app/data 25 | ENV PASTE_ENABLE_CHARTS 1 26 | EXPOSE 8080 27 | 28 | ENTRYPOINT ["/usr/bin/perl", "/app-mojopaste-main/script/mojopaste", "prefork", "-l", "http://*:8080"] 29 | -------------------------------------------------------------------------------- /t/00-basic.t: -------------------------------------------------------------------------------- 1 | use Test::More; 2 | use File::Find; 3 | 4 | if(($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/) { 5 | plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/'; 6 | } 7 | if(!eval 'use Test::Pod; 1') { 8 | *Test::Pod::pod_file_ok = sub { SKIP: { skip "pod_file_ok(@_) (Test::Pod is required)", 1 } }; 9 | } 10 | if(!eval 'use Test::Pod::Coverage; 1') { 11 | *Test::Pod::Coverage::pod_coverage_ok = sub { SKIP: { skip "pod_coverage_ok(@_) (Test::Pod::Coverage is required)", 1 } }; 12 | } 13 | if(!eval 'use Test::CPAN::Changes; 1') { 14 | *Test::CPAN::Changes::changes_file_ok = sub { SKIP: { skip "changes_ok(@_) (Test::CPAN::Changes is required)", 4 } }; 15 | } 16 | 17 | find( 18 | { 19 | wanted => sub { /\.pm$/ and push @files, $File::Find::name }, 20 | no_chdir => 1 21 | }, 22 | -e 'blib' ? 'blib' : 'lib', 23 | ); 24 | 25 | plan tests => @files * 3 + 4; 26 | 27 | for my $file (@files) { 28 | my $module = $file; $module =~ s,\.pm$,,; $module =~ s,.*/?lib/,,; $module =~ s,/,::,g; 29 | ok eval "use $module; 1", "use $module" or diag $@; 30 | Test::Pod::pod_file_ok($file); 31 | Test::Pod::Coverage::pod_coverage_ok($module, { also_private => [ qr/^[A-Z_]+$/ ], }); 32 | } 33 | 34 | Test::CPAN::Changes::changes_file_ok(); 35 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v*.* 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Calculate tag name 13 | id: calculate_tag 14 | run: | 15 | if echo $GITHUB_REF | grep -q '^refs/tags/v'; then 16 | VERSION=$(echo $GITHUB_REF | cut -d/ -f3); 17 | TAG_NAME="${{ secrets.DOCKER_HUB_PREFIX }}/mojopaste:$VERSION"; 18 | else 19 | TAG_NAME="${{ secrets.DOCKER_HUB_PREFIX }}/mojopaste:latest"; 20 | fi 21 | echo '::set-output name=tag::'$TAG_NAME 22 | - name: Check Out Repo 23 | uses: actions/checkout@v1 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 28 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v1 31 | - name: Set up Docker Buildx 32 | id: buildx 33 | uses: docker/setup-buildx-action@v1 34 | - name: Build and push 35 | id: docker_build 36 | uses: docker/build-push-action@v2 37 | with: 38 | context: ./ 39 | file: ./Dockerfile 40 | push: true 41 | platforms: linux/amd64 42 | tags: "${{ steps.calculate_tag.outputs.tag }}" 43 | - name: Image digest 44 | run: echo ${{ steps.docker_build.outputs.digest }} 45 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | # Generated by git-ship. See 'git-ship --man' for help or https://github.com/jhthorsen/app-git-ship 2 | use utf8; 3 | use ExtUtils::MakeMaker; 4 | my %WriteMakefileArgs = ( 5 | NAME => 'App::mojopaste', 6 | AUTHOR => 'Jan Henning Thorsen ', 7 | LICENSE => 'artistic_2', 8 | ABSTRACT_FROM => 'lib/App/mojopaste.pm', 9 | VERSION_FROM => 'lib/App/mojopaste.pm', 10 | EXE_FILES => [qw(script/mojopaste)], 11 | OBJECT => '', 12 | BUILD_REQUIRES => {}, 13 | TEST_REQUIRES => {'Test::More' => '0.88'}, 14 | PREREQ_PM => {'Mojolicious' => '9.11', 'Text::CSV' => '1.30'}, 15 | META_MERGE => { 16 | 'dynamic_config' => 0, 17 | 'meta-spec' => {version => 2}, 18 | 'resources' => { 19 | bugtracker => {web => 'https://github.com/jhthorsen/app-mojopaste/issues'}, 20 | homepage => 'https://github.com/jhthorsen/app-mojopaste', 21 | repository => { 22 | type => 'git', 23 | url => 'https://github.com/jhthorsen/app-mojopaste.git', 24 | web => 'https://github.com/jhthorsen/app-mojopaste', 25 | }, 26 | }, 27 | 'x_contributors' => ['Jan Henning Thorsen '], 28 | }, 29 | test => {TESTS => (-e 'META.yml' ? 't/*.t' : 't/*.t xt/*.t')}, 30 | ); 31 | 32 | unless (eval { ExtUtils::MakeMaker->VERSION('6.63_03') }) { 33 | my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES}; 34 | @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires; 35 | } 36 | 37 | WriteMakefile(%WriteMakefileArgs); 38 | -------------------------------------------------------------------------------- /t/charts.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{PASTE_ENABLE_CHARTS} = 1 } 2 | use lib '.'; 3 | use t::Helper; 4 | use Mojo::JSON 'true'; 5 | 6 | my $t = t::Helper->t; 7 | my ($raw, $file, $json); 8 | 9 | plan skip_all => "$ENV{PASTE_DIR} was not created" unless -d $ENV{PASTE_DIR}; 10 | 11 | $raw = <<"HERE"; 12 | 13 | # 14 | # Some cool header 15 | # 16 | # A wonderful description. 17 | # 18 | 19 | Date,Down,Up 20 | 2015-02-04 15:03,120,90 21 | 2015-03-14,75,65 22 | 23 | # this is a bit weird...? 24 | 2015-04,100,40 25 | 26 | # 27 | HERE 28 | $t->post_ok('/', form => {paste => $raw, p => 1})->status_is(302); 29 | $file = $t->tx->res->headers->location =~ m!/(\w+)$! ? $1 : 'nope'; 30 | 31 | $t->get_ok("/$file/chart")->status_is(200)->text_is(title => 'Some cool header - Mojopaste graph') 32 | ->content_like(qr{jquery\.min\.js})->content_like(qr{morris\.min\.js})->content_like(qr{raphael-min\.js}) 33 | ->element_exists('div[id="chart"]')->element_exists('nav')->text_like('h2', qr{Some cool header}, 'header') 34 | ->text_like('p', qr{A wonderful description\.}, 'description'); 35 | 36 | $json = $t->tx->res->body =~ m!new Morris\.Line\(([^\)]+)\)! ? Mojo::JSON::decode_json($1) : undef; 37 | is_deeply( 38 | $json, 39 | { 40 | element => 'chart', 41 | hideHover => true, 42 | labels => [qw(Down Up)], 43 | pointStrokeColors => '#222', 44 | resize => true, 45 | xkey => 'Date', 46 | ykeys => [qw(Down Up)], 47 | data => [ 48 | {Date => '2015-02-04 15:03', Down => 120, Up => 90}, 49 | {Date => '2015-03-14', Down => 75, Up => 65}, 50 | {Date => '2015-04', Down => 100, Up => 40} 51 | ], 52 | }, 53 | 'got correct graph data', 54 | ) or diag $t->tx->res->body; 55 | 56 | unlink glob("$ENV{PASTE_DIR}/*"); 57 | 58 | done_testing; 59 | -------------------------------------------------------------------------------- /t/mojopaste.t: -------------------------------------------------------------------------------- 1 | use lib '.'; 2 | use t::Helper; 3 | 4 | my $t = t::Helper->t; 5 | my $raw = "// somefile.js\nvar foo = 123; // cool!\r\nvar toooooooo_long_for_title = 1234567890;\r\n"; 6 | 7 | $t->get_ok('/')->status_is(200)->text_is('title', 'Create new paste - Mojopaste') 8 | ->element_exists('form[method="post"][action="invalid"]', 'javascript is required')->element_exists('button'); 9 | 10 | $t->post_ok('/')->status_is(400)->element_exists('form[method="post"][action="invalid"]'); 11 | $t->post_ok('/', form => {paste => '', p => 1})->status_is(400, 'Need at least one character'); 12 | 13 | $t->post_ok('/', form => {paste => $raw, p => 1})->status_is(302)->header_like('Location', qr[^/\w{12}$]); 14 | 15 | my ($id) = $t->tx->res->headers->location =~ m!/(\w+)$!; 16 | $t->get_ok($t->tx->res->headers->location)->status_is(200) 17 | ->text_is('title', 'somefile.js var foo = 123; // cool! var toooooo - Mojopaste')->element_exists(qq(a[href="/"])) 18 | ->element_exists(qq(a[href="/$id.txt"]))->element_exists(qq(a[href="/?edit=$id"])) 19 | ->element_exists_not(qq(a[href\$="/chart"])) # $ENV{PASTE_ENABLE_CHARTS} is not set 20 | ->element_exists('pre')->text_is('pre', $raw); 21 | 22 | # $ENV{PASTE_ENABLE_CHARTS} is not set 23 | $t->get_ok("/$id/chart")->status_is(404); 24 | $t->get_ok("/$id")->status_is(200)->header_like('X-Plain-Text-URL', qr{:\d+/$id\.txt$})->element_exists('nav'); 25 | $t->get_ok("/$id?embed=text")->status_is(200)->element_exists_not('nav'); 26 | $t->get_ok("/$id.txt")->content_is($raw); 27 | ok !$t->tx->res->headers->header('X-Plain-Text-URL'), 'no X-Plain-Text-URL'; 28 | 29 | $t->get_ok("/?edit=$id")->text_is('title', 'somefile.js var foo = 123; // cool! var to - Mojopaste edit') 30 | ->text_is('textarea', $raw); 31 | 32 | require File::Path; 33 | File::Path::remove_tree($ENV{PASTE_DIR}, {keep_root => 1}); 34 | done_testing; 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mojopaste - Pastebin application 2 | 3 | Mojopaste is a pastebin application. There's about one million of these out 4 | there, but if you have the need to run something internally at work or you 5 | just fancy having your own pastebin, this is your application. 6 | 7 | # Text and code 8 | 9 | The standard version of [App::mojopaste](https://metacpan.org/pod/App::mojopaste) can take normal text as input, 10 | store it as a text file on the server and render the content as either 11 | plain text or prettified using [Google prettify](https://code.google.com/p/google-code-prettify/). 12 | (Note: Maybe another prettifier will be used in future versions) 13 | 14 | # Charts 15 | 16 | In addition to just supporting text, this application can also make charts 17 | from the input data. To turn this feature on, you need to specify 18 | "enable\_charts" in the config or set the `PASTE_ENABLE_CHARTS` 19 | environment variable: 20 | 21 | $ PASTE_ENABLE_CHARTS=1 script/mojopaste daemon; 22 | 23 | The input chart data must be valid CSV: 24 | 25 | CSV data is similar to ["Just data"](#just-data) above, except the first line is used as 26 | "xkey,ykey1,ykey2,...". Example: 27 | 28 | # Can have comments in CSV input as well 29 | x,a,b 30 | 2015-02-04 15:03,120,90 31 | 2015-03-14,75,65 32 | 2015-04,100,40 33 | 34 | CSV input data require [Text::CSV](https://metacpan.org/pod/Text::CSV) to be installed. 35 | 36 | # Embedding 37 | 38 | A paste can be embedded in other pages using the query param "embed". Examples: 39 | 40 | - [http://p.thorsen.pm/mojopastedemo.txt](http://p.thorsen.pm/mojopastedemo.txt) 41 | Get the raw data. 42 | - [http://p.thorsen.pm/mojopastedemo?embed=text](http://p.thorsen.pm/mojopastedemo?embed=text) 43 | Show the paste without any margin/padding and no menu. 44 | - [http://p.thorsen.pm/mojopastedemo/chart?embed=graph](http://p.thorsen.pm/mojopastedemo/chart?embed=graph) 45 | Show only the graph data. 46 | - [http://p.thorsen.pm/mojopastedemo/chart?embed=graph,heading,description](http://p.thorsen.pm/mojopastedemo/chart?embed=graph,heading,description) 47 | Show the graph data, heading and description, but no menus. 48 | 49 | # Demo 50 | 51 | You can try mojopaste here: [http://p.thorsen.pm](http://p.thorsen.pm). 52 | 53 | # Installation 54 | 55 | Install system wide with cpanm: 56 | 57 | $ cpanm --sudo App::mojopaste 58 | 59 | # Docker run 60 | 61 | It is possible to install [mojopaste](https://hub.docker.com/r/jhthorsen/mojopaste) 62 | using Docker: 63 | 64 | $ mkdir /var/lib/mojopaste 65 | $ docker run -d --restart always --name mojopaste \ 66 | -v /var/lib/mojopaste:/app/data -p 3000:8080 jhthorsen/mojopaste 67 | 68 | # Synopsis 69 | 70 | - Simple single process daemon 71 | 72 | $ mojopaste daemon --listen http://*:8080 73 | 74 | - Save paste to custom dir 75 | 76 | $ PASTE_DIR=/path/to/paste/dir mojopaste daemon --listen http://*:8080 77 | 78 | - Using the UNIX optimized, preforking hypnotoad web server 79 | 80 | $ MOJO_CONFIG=/path/to/mojopaste.conf hypnotoad $(which mojopaste) 81 | 82 | Example mojopaste.conf: 83 | 84 | { 85 | paste_dir => '/path/to/paste/dir', 86 | enable_charts => 1, # default is 0 87 | hypnotoad => { 88 | listen => ['http://*:8080'], 89 | }, 90 | } 91 | 92 | "enable\_charts" is for adding a button which can make a chart of the input 93 | data using [morris.js](http://morrisjs.github.io/morris.js) 94 | 95 | Check out [Mojo::Server::Hypnotoad](https://metacpan.org/pod/Mojo::Server::Hypnotoad) for more hypnotoad options. 96 | -------------------------------------------------------------------------------- /lib/App/mojopaste.pm: -------------------------------------------------------------------------------- 1 | package App::mojopaste; 2 | 3 | our $VERSION = '1.05'; 4 | 5 | 1; 6 | 7 | =encoding utf8 8 | 9 | =head1 NAME 10 | 11 | App::mojopaste - Pastebin application 12 | 13 | =head1 VERSION 14 | 15 | 1.05 16 | 17 | =head1 DESCRIPTION 18 | 19 | Mojopaste is a pastebin application. There's about one million of these out 20 | there, but if you have the need to run something internally at work or you 21 | just fancy having your own pastebin, this is your application. 22 | 23 | =head2 Text and code 24 | 25 | The standard version of L can take normal text as input, 26 | store it as a text file on the server and render the content as either 27 | plain text or prettified using L. 28 | (Note: Maybe another prettifier will be used in future versions) 29 | 30 | =head2 Charts 31 | 32 | In addition to just supporting text, this application can also make charts 33 | from the input data. To turn this feature on, you need to specify 34 | "enable_charts" in the config or set the C 35 | environment variable: 36 | 37 | $ PASTE_ENABLE_CHARTS=1 script/mojopaste daemon; 38 | 39 | The input chart data must be valid CSV: 40 | 41 | CSV data is similar to L above, except the first line is used as 42 | "xkey,ykey1,ykey2,...". Example: 43 | 44 | # Can have comments in CSV input as well 45 | x,a,b 46 | 2015-02-04 15:03,120,90 47 | 2015-03-14,75,65 48 | 2015-04,100,40 49 | 50 | CSV input data require L to be installed. 51 | 52 | =head2 Embedding 53 | 54 | A paste can be embedded in other pages using the query param "embed". Examples: 55 | 56 | =over 2 57 | 58 | =item * L 59 | 60 | Get the raw data. 61 | 62 | =item * L 63 | 64 | Show the paste without any margin/padding and no menu. 65 | 66 | =item * L 67 | 68 | Show only the graph data. 69 | 70 | =item * L 71 | 72 | Show the graph data, heading and description, but no menus. 73 | 74 | =back 75 | 76 | =head1 DEMO 77 | 78 | You can try mojopaste here: L. 79 | 80 | =head1 INSTALLATION 81 | 82 | Install system wide with cpanm: 83 | 84 | $ cpanm --sudo App::mojopaste 85 | 86 | Don't have cpanm installed? 87 | 88 | $ curl -L http://cpanmin.us | perl - --sudo App::mojopaste 89 | $ wget http://cpanmin.us -O - | perl - --sudo App::mojopaste 90 | 91 | It is also possible to install L using Docker. Check out 92 | L for more information. 93 | 94 | =head1 SYNOPSIS 95 | 96 | =over 2 97 | 98 | =item * Simple single process daemon 99 | 100 | $ mojopaste daemon --listen http://*:8080 101 | 102 | =item * Save paste to custom dir 103 | 104 | $ PASTE_DIR=/path/to/paste/dir mojopaste daemon --listen http://*:8080 105 | 106 | =item * Using the UNIX optimized, preforking hypnotoad web server 107 | 108 | $ MOJO_CONFIG=/path/to/mojopaste.conf hypnotoad $(which mojopaste) 109 | 110 | Example mojopaste.conf: 111 | 112 | { 113 | brand_link => "index", 114 | brand_logo => "/images/logo.png", 115 | brand_text => "Mojopaste", 116 | paste_dir => "/path/to/paste/dir", 117 | enable_charts => 1, # default is 0 118 | hypnotoad => { 119 | listen => ["http://*:8080"], 120 | }, 121 | } 122 | 123 | "enable_charts" is for adding a button which can make a chart of the input 124 | data using L 125 | 126 | Check out L for more hypnotoad options. 127 | 128 | =back 129 | 130 | =head1 OTHER PASTEBINS 131 | 132 | =over 2 133 | 134 | =item * L 135 | 136 | =item * L 137 | 138 | =item * L 139 | 140 | =back 141 | 142 | =head1 AUTHOR 143 | 144 | Jan Henning Thorsen - C 145 | 146 | =cut 147 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for perl distribution App-mojopaste 2 | 3 | 1.06 Not Released 4 | - Add support for X-Request-Base 5 | 6 | 1.05 2021-06-17T07:56:14+0900 7 | - Compatible with Mojolicious 9.11 8 | Contributor: Dan Book 9 | 10 | 1.04 2019-08-09T16:37:47+0200 11 | - Fix failing tests #19 12 | 13 | 1.03 2019-08-08T23:51:56+0200 14 | - Add support for custom branding 15 | 16 | 1.02 2019-08-08T21:47:21+0200 17 | - Fix height of textarea 18 | 19 | 1.01 2019-08-08T21:25:41+0200 20 | - Changed layout to "dark mode" 21 | - Changed layout to using "modern" CSS technologies, such as var() 22 | - Swapped Google Code Prettify style to 23 | https://jmblog.github.io/color-themes-for-google-code-prettify/themes/atelier-dune-dark.min.css 24 | - Using a modified version of https://commons.wikimedia.org/wiki/File:Gnome-edit-paste.svg 25 | as logo, under the CC BY-SA 3.0 license. 26 | - Add basic documentation to script #17 27 | Contributor: Dan Book 28 | - Fix shebang #16 29 | Contributor: Dan Book 30 | 31 | 1.00 2018-09-26T00:00:33+0900 32 | - Compatible with Mojolicious 8.00 33 | - Changed from delay to promises 34 | 35 | 0.26 2018-04-21T10:37:22+0200 36 | - Add title that describe which paste you look at 37 | - Add support for pluggable backends #14 38 | - Change Dockerfile to pull from github.com/jhthorsen/app-mojopaste instead of cpan 39 | - Fix "...did you mean do "./script/mojopaste"? at t/config.t line 10." 40 | 41 | 0.25 2017-06-20T00:20:46+0200 42 | - Mojolicious has deprecated Mojo::Util::slurp() and spurt() 43 | - Fix failing tests 44 | 45 | 0.24 2016-10-13T10:01:53+0200 46 | - Writing/reading files are more robust 47 | 48 | 0.23 2016-09-08T20:08:22+0200 49 | - Fix rendering 404 for paste_not_found() and .txt extension 50 | 51 | 0.22 2016-09-06T21:18:39+0200 52 | - Will never allow robots 53 | - Will only graph csv data 54 | 55 | 0.21 2016-06-30T14:44:51+0200 56 | - Change default paste directory 57 | - Can embed paste and graphs 58 | - Fix using the whole window and nothing more when showing a graph 59 | 60 | 0.20 2016-06-29T18:08:30+0200 61 | - Fix X-Plain-Text-URL must be an absolute URL 62 | - Add line numbers to pretty printed code 63 | - Change "Paste" button to "Save" 64 | 65 | 0.19 2016-06-29T13:59:26+0200 66 | - Fix selecting only pastebin text on ctrl+a or cmd+a 67 | - Fix handling of empty paste 68 | - Add EXPERIMENTAL X-Plain-Text-URL header to paste web page 69 | - Add support for header and description for graph view #7 70 | - Change from dark to light color theme 71 | 72 | 0.18 2016-01-15T07:52:58+0100 73 | - Try to fix failing MSWin32 test 74 | http://www.cpantesters.org/cpan/report/873ffedb-6bf4-1014-a355-7b0739136c96 75 | 76 | 0.17 2016-01-13T21:36:27+0100 77 | - Fix unicode.t by bumping Mojolicious version #10 78 | - Cannot make empty paste 79 | - Non-existing paste will result in 404 instead of 500 #8 80 | Contributor: Stephan Jauernick 81 | - Use File::Spec to construct correct paths on all OS #8 82 | Contributor: Stephan Jauernick 83 | 84 | 0.16 2016-01-13T09:25:51+0100 85 | - Recommends JSON::Syck 86 | - Recommends Text::CSV 87 | - No need to encode/decode UTF-8 anymore 88 | 89 | 0.15 2015-04-30T10:17:13+0200 90 | - Change "Powered by" URL 91 | - Change styling for .morris-hover 92 | - Change 404 page to show that the paste could not be found. 93 | 94 | 0.14 2015-04-20T17:03:36+0200 95 | - Fix unit tests 96 | 97 | 0.13 2015-04-20T16:54:30+0200 98 | - SECURITY FIX! Cannot read ../../private/file #6 99 | - Capture chart errors 100 | - Will construct default ykeys from all chart elements 101 | 102 | 0.11 2015-04-20T15:15:43+0200 103 | - Can make charts using morris.js 104 | 105 | 0.10 2014-08-07T17:51:23Z 106 | - Fix button border styling, #2 107 | 108 | 0.09 2014-08-07T11:14:00Z 109 | - Add support for .txt as raw format 110 | - Add focus on textarea on load 111 | - Change to plain javascript (no jQuery) 112 | - New design with bottom navbar 113 | 114 | 0.08 2014-07-22T17:00:15Z 115 | - Compatible with Mojolicious 5.xx 116 | 117 | 0.07 2014-07-21T06:52:09Z 118 | - Compatible with Mojolicious 5.xx 119 | 120 | 0.06 2014-07-18T02:13:05Z 121 | - Change bugracker 122 | 123 | 0.0501 2014-05-30T08:02:31Z 124 | - Fix fragile test t/mojopaste.t 125 | 126 | 0.05 2013-12-31T19:39:07Z 127 | - Allow pasting without javascript enabled 128 | 129 | 0.0403 2013-09-09T16:27:19Z 130 | - Fix showing unicode characters 131 | 132 | 0.0402 2013-09-09T16:08:21Z 133 | - Fix pasting unicode characters 134 | 135 | 0.0401 2013-09-06T23:34:51Z 136 | - Fix Makefile.PL 137 | 138 | 0.04 2013-09-05T23:55:30Z 139 | - Fix including jquery.js from Mojo 140 | - Render as HTML5 141 | - Looks better on iPhone 142 | - Render 404 when paste is not found 143 | - Trying to trick the dumbest robots 144 | 145 | 0.03 2013-09-05T16:54:59Z 146 | - Fix typo in PREREQ_PM 147 | 148 | 0.02 2013-09-05T16:48:08Z 149 | - Fix repository path 150 | 151 | 0.01 2013-09-05T16:46:19Z 152 | - Add bin/mojopaste 153 | - Add documentation 154 | 155 | -------------------------------------------------------------------------------- /script/mojopaste: -------------------------------------------------------------------------------- 1 | #!perl 2 | package App::mojopaste::Backend::File; 3 | use Mojo::Base 'Mojolicious::Plugin'; 4 | 5 | use Mojo::File 'path'; 6 | use Mojo::Util qw(encode decode); 7 | use Text::CSV; 8 | 9 | my $ID = 0; 10 | 11 | sub register { 12 | my ($self, $app, $config) = @_; 13 | my $dir = $app->config('paste_dir'); 14 | path($dir)->make_path unless -d $dir; 15 | $app->helper('paste.load_p' => sub { _load_p($dir, @_) }); 16 | $app->helper('paste.save_p' => sub { _save_p($dir, @_) }); 17 | } 18 | 19 | sub _load_p { 20 | my ($dir, $c, $id) = @_; 21 | my @res = ('', ''); 22 | 23 | eval { 24 | die "Hacking attempt! paste_id=($id)" if !$id or $id =~ m!\W!; 25 | return Mojo::Promise->new->resolve(decode 'UTF-8', path($dir, $id)->slurp); 26 | } or do { 27 | return Mojo::Promise->new->reject($@ || 'Paste not found'); 28 | }; 29 | } 30 | 31 | sub _save_p { 32 | my ($dir, $c, $text) = @_; 33 | my $id = substr Mojo::Util::md5_sum($$ . time . $ID++), 0, 12; 34 | my @res = ('', ''); 35 | 36 | eval { 37 | path($dir, $id)->spurt(encode 'UTF-8', $text); 38 | return Mojo::Promise->new->resolve($id); 39 | } or do { 40 | return Mojo::Promise->new->reject($@ || 'Unknown error'); 41 | }; 42 | } 43 | 44 | package main; 45 | use Mojolicious::Lite; 46 | 47 | use Mojo::JSON 'true'; 48 | 49 | plugin 'config' if $ENV{MOJO_CONFIG}; 50 | app->config->{backend} ||= $ENV{PASTE_BACKEND} || 'File'; 51 | app->config->{paste_dir} ||= $ENV{PASTE_DIR} || 'paste'; 52 | 53 | app->defaults( 54 | brand_link => app->config('brand_link') || $ENV{PASTE_BRAND_LINK} || 'index', 55 | brand_logo => app->config('brand_logo') // $ENV{PASTE_BRAND_LOGO} // '/images/logo.png', 56 | brand_name => app->config('brand_name') // $ENV{PASTE_BRAND_NAME} // 'Mojopaste', 57 | enable_charts => app->config('enable_charts') // $ENV{PASTE_ENABLE_CHARTS}, 58 | embed => 'description,graph,heading,nav', 59 | error => '', 60 | paste => '', 61 | placeholder => 'Enter your text here and then press the "Save" button above.', 62 | title => 'Mojopaste', 63 | ); 64 | 65 | my $backend = app->config('backend'); 66 | plugin $backend =~ /::/ ? $backend : "App::mojopaste::Backend::$backend"; 67 | 68 | helper no_such_paste => sub { 69 | my ($c, $err) = @_; 70 | $c->app->log->debug("no_such_paste: $err"); 71 | $c->stash($_ => 'Could not find paste') for qw(error heading title); 72 | $c->render(description => '', layout => 'mojopaste', status => 404); 73 | }; 74 | 75 | helper set_title => sub { 76 | my ($c, $prefix, $suffix) = @_; 77 | my $brand_name = $c->stash('brand_name') || 'Mojopaste'; 78 | $suffix = $suffix ? "$brand_name $suffix" : $brand_name; 79 | $prefix =~ s![\n\r]+! !g; 80 | $prefix =~ s!^\W+!!g; 81 | $prefix = substr $prefix, 0, 56 - length $suffix; 82 | return $c->stash(title => "$prefix - $suffix"); 83 | }; 84 | 85 | get( 86 | '/' => {layout => 'mojopaste'} => sub { 87 | my $c = shift; 88 | 89 | return $c->set_title("Create new paste") unless my $id = $c->param('edit'); 90 | return $c->render_later->paste->load_p($id)->then(sub { 91 | return $c->no_such_paste('Could not find paste') unless my $paste = shift; 92 | $c->set_title(substr($paste, 0, 80), 'edit'); 93 | $c->param(paste => $paste)->render; 94 | })->catch(sub { $c->no_such_paste(shift) }); 95 | }, 96 | 'index' 97 | ); 98 | 99 | post( 100 | '/' => {layout => 'mojopaste'}, 101 | sub { 102 | my $c = shift; 103 | my $paste = $c->param('paste') || ''; 104 | 105 | return $c->render('index', placeholder => 'You neeed to enter some characters!', status => 400) 106 | unless $paste =~ /\w/; 107 | return $c->render_later->paste->save_p($paste)->then(sub { 108 | $c->redirect_to('show', paste_id => shift); 109 | })->catch(sub { $c->reply->exception(shift) }); 110 | } 111 | ); 112 | 113 | get( 114 | '/:paste_id', 115 | [format => ['html', 'txt']], 116 | {format => undef}, 117 | sub { 118 | my $c = shift; 119 | my $format = $c->stash('format') || ''; 120 | 121 | $c->render_later->paste->load_p($c->stash('paste_id'))->then(sub { 122 | my $paste = shift; 123 | if (!$paste) { 124 | $c->no_such_paste('Could not find paste'); 125 | } 126 | elsif ($c->param('raw') or $format eq 'txt') { 127 | $c->res->headers->content_type('text/plain; charset=utf-8'); 128 | $c->render(text => $paste); 129 | } 130 | else { 131 | $c->set_title(substr($paste, 0, 80)); 132 | $c->res->headers->header('X-Plain-Text-URL' => $c->url_for(format => 'txt')->userinfo(undef)->to_abs); 133 | $c->stash(embed => $c->param('embed')) if $c->param('embed'); 134 | $c->render(layout => 'mojopaste', paste => $paste); 135 | } 136 | })->catch(sub { $c->no_such_paste(shift) }); 137 | }, 138 | 'show' 139 | ); 140 | 141 | app->defaults('enable_charts') and get( 142 | '/:paste_id/chart' => {layout => 'mojopaste'}, 143 | sub { 144 | my $c = shift; 145 | my $chart = {element => 'chart', data => [], hideHover => true, resize => true}; 146 | my ($heading, $description, $error) = ('', '', ''); 147 | 148 | $c->render_later->paste->load_p($c->stash('paste_id'))->then(sub { 149 | return $c->no_such_paste('Could not find paste') unless my $paste = shift; 150 | 151 | while ($paste =~ s!^\s*(?://|\#)(.*)!!m) { 152 | $description .= $1 if $heading; 153 | $heading ||= $1; 154 | } 155 | 156 | eval { 157 | _chart($chart, grep { $_ =~ /\S/ } split /\r?\n/, $paste); 158 | } or do { 159 | $error = $@ || 'Unknown error'; 160 | $error =~ s!\s*at .*? line \d+.*!!s; 161 | }; 162 | 163 | $c->set_title($heading || $description || substr($paste, 0, 80), 'graph'); 164 | $c->stash(embed => $c->param('embed')) if $c->param('embed'); 165 | $c->render(chart => $chart, description => $description // '', error => $error, heading => $heading); 166 | })->catch(sub { $c->no_such_paste(shift) }); 167 | }, 168 | 'chart' 169 | ); 170 | 171 | hook before_dispatch => sub { 172 | my $c = shift; 173 | return unless $ENV{X_REQUEST_BASE} and my $base = $c->req->headers->header('X-Request-Base'); 174 | $c->req->url->base(Mojo::URL->new($base)); 175 | }; 176 | 177 | app->start; 178 | 179 | sub _chart { 180 | my $chart = shift; 181 | my $csv = Text::CSV->new; 182 | 183 | $csv->parse(shift @_); # heading 184 | $chart->{ykeys} = [$csv->fields]; 185 | $chart->{xkey} = shift @{$chart->{ykeys}}; 186 | $chart->{labels} = $chart->{ykeys}; 187 | $chart->{pointStrokeColors} = '#222'; 188 | 189 | while (@_) { 190 | die $csv->error_input unless $csv->parse(shift @_); 191 | my @row = $csv->fields or next; 192 | push @{$chart->{data}}, {$chart->{xkey} => shift(@row), map { ($_ => 0 + shift @row) } @{$chart->{ykeys}}}; 193 | } 194 | 195 | die 'Could not parse CSV data.' unless @{$chart->{data}}; 196 | return $chart; 197 | } 198 | 199 | =pod 200 | 201 | =encoding utf8 202 | 203 | =head1 NAME 204 | 205 | mojopaste - Pastebin application 206 | 207 | =head1 DESCRIPTION 208 | 209 | See L. 210 | 211 | =head1 AUTHOR 212 | 213 | Jan Henning Thorsen - C 214 | 215 | =cut 216 | 217 | __DATA__ 218 | @@ layouts/mojopaste.html.ep 219 | 220 | 221 | 222 | <%= title %> 223 | 224 | 225 | %= stylesheet begin 226 | :root { 227 | --root-bg-color: #1d1e19; 228 | --root-font-color: #d5d9bc; 229 | --root-font-size: 16px; 230 | --root-font-family: Menlo, Bitstream Vera Sans Mono, DejaVu Sans Mono, Monaco, Consolas, monospace; 231 | --gutter: 2rem; 232 | --nav-bg-color: #191a15; 233 | --nav-height: 3rem; 234 | } 235 | 236 | @media (max-width: 800px) { 237 | :root { 238 | --gutter: 1rem; 239 | } 240 | } 241 | 242 | * { 243 | border: 0; 244 | padding: 0; 245 | margin: 0; 246 | box-sizing: border-box; 247 | } 248 | 249 | html, body, textarea { 250 | background: var(--root-bg-color); 251 | } 252 | 253 | html, body, textarea, button { 254 | font-family: var(--root-font-family); 255 | font-size: var(--root-font-size); 256 | color: var(--root-font-color); 257 | } 258 | 259 | h2 { 260 | margin: var(--gutter); 261 | margin-bottom: 1rem; 262 | } 263 | 264 | p { 265 | margin: 1rem var(--gutter); 266 | } 267 | 268 | a { 269 | color: var(--root-font-color); 270 | } 271 | 272 | a:hover, 273 | .btn:hover { 274 | background-color: #11120f; 275 | } 276 | 277 | nav { 278 | background: var(--nav-bg-color); 279 | position: sticky; 280 | top: 0; 281 | height: var(--nav-height); 282 | box-shadow: -1px 0 0 1px rgba(0, 0, 0, 0.2); 283 | } 284 | 285 | .btn, 286 | .brand { 287 | --padding: 0.4rem; 288 | 289 | background: var(--nav-bg-color); 290 | text-decoration: none; 291 | line-height: calc(var(--nav-height) - var(--padding) * 2); 292 | margin-left: calc(var(--gutter) / 2); 293 | padding: var(--padding) calc(var(--gutter) / 2); 294 | float: left; 295 | display: block; 296 | cursor: pointer; 297 | } 298 | 299 | .brand { 300 | display: block !important; 301 | } 302 | 303 | .brand img { 304 | height: calc(var(--nav-height) - var(--padding) * 2); 305 | vertical-align: bottom; 306 | } 307 | 308 | .editor, 309 | .prettyprint { 310 | padding: var(--gutter); 311 | width: 100vw; 312 | max-width: 100vw; 313 | overflow: scroll; 314 | outline: 0; 315 | -webkit-overflow-scrolling: touch; 316 | } 317 | 318 | .has-nav .editor, 319 | .has-nav .prettyprint { 320 | height: calc(100vh - var(--nav-height)); 321 | } 322 | 323 | .prettyprint.linenums { 324 | padding-left: 0; 325 | } 326 | 327 | .linenums { 328 | padding-left: 3.5em; 329 | } 330 | 331 | @media (min-width: 800px) { 332 | .linenums { 333 | padding-left: 4.5em; 334 | } 335 | } 336 | 337 | @media print { 338 | nav a, 339 | nav .btn { 340 | display: none; 341 | } 342 | 343 | .prettyprint { 344 | white-space: pre-wrap; 345 | } 346 | } 347 | 348 | /*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */ 349 | .prettyprint{font-family:Menlo,Bitstream Vera Sans Mono,DejaVu Sans Mono,Monaco,Consolas,monospace;border:0!important}.pln{color:#c5c8c6}ol.linenums{margin-top:0;margin-bottom:0;color:#969896}li.L0,li.L1,li.L2,li.L3,li.L4,li.L5,li.L6,li.L7,li.L8,li.L9{list-style-type:decimal}@media screen{.str{color:#b5bd68}.kwd{color:#b294bb}.com{color:#969896}.typ{color:#81a2be}.lit{color:#de935f}.pun{color:#c5c8c6}.opn{color:#c5c8c6}.clo{color:#c5c8c6}.tag{color:#c66}.atn{color:#de935f}.atv{color:#8abeb7}.dec{color:#de935f}.var{color:#c66}.fun{color:#81a2be}} 350 | 351 | .morris-hover{position:absolute;z-index:1000} 352 | .morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(0,0,0,0.8);font-size:12px;text-align:center} 353 | .morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0} 354 | .morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0} 355 | 356 | % end 357 | 419 | 420 | 421 | 422 | %= content 423 | 424 | 425 | @@ layouts/mojopaste.txt.ep 426 | %= content 427 | @@ index.html.ep 428 | %= form_for 'invalid', method => 'post', begin 429 | %= include 'nav' 430 | %= text_area 'paste', placeholder => $placeholder, tabindex => 1, class => 'editor' 431 | % end 432 | @@ chart.html.ep 433 | %= include 'nav' if $embed =~ /nav/; 434 | % if ($heading and $embed =~ /heading/) { 435 |

<%= $heading %>

436 | % } 437 | % if ($description and $embed =~ /description/) { 438 |

<%= $description %>

439 | % } 440 | % if ($embed =~ /graph/) { 441 |
<%= $error %>
442 | 443 | 444 | 445 | % unless ($error) { 446 | 457 | % } 458 | % } 459 | @@ show.html.ep 460 | %= include 'nav' if $embed =~ /nav/; 461 |
<%= $error || $paste %>
462 | @@ nav.html.ep 463 | % my $paste_id = stash 'paste_id'; 464 | 489 | @@ show.txt.ep 490 | Paste not found: <%= $paste_id %> 491 | @@ images/logo.png (base64) 492 | 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 558 | --------------------------------------------------------------------------------