ciridiri 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +22 -0
- data/Rakefile +36 -0
- data/config.ru +13 -0
- data/lib/ciridiri.rb +48 -0
- data/lib/ciridiri/extensions.rb +48 -0
- data/lib/ciridiri/finders.rb +24 -0
- data/lib/ciridiri/page.rb +149 -0
- data/lib/ciridiri/paths.rb +12 -0
- data/public/css/base.css +240 -0
- data/public/css/print.css +31 -0
- data/public/js/application.js +22 -0
- data/test/ciridiri_test.rb +81 -0
- data/test/page_test.rb +101 -0
- data/test/test_helper.rb +28 -0
- data/views/edit.erb +16 -0
- data/views/layout.erb +26 -0
- data/views/show.erb +3 -0
- metadata +132 -0
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#ciridiri.rb
|
2
|
+
|
3
|
+
__ciridiri.rb__ is a Ruby+Sinatra port of [ciridiri](http://vast.github.com/ciridiri/),
|
4
|
+
dead simple wiki engine.
|
5
|
+
|
6
|
+
##Requirements
|
7
|
+
|
8
|
+
* [sinatra][]
|
9
|
+
|
10
|
+
##Installation
|
11
|
+
|
12
|
+
git clone git://github.com/vast/ciridiri.rb.git
|
13
|
+
cd ciridiri.rb
|
14
|
+
rackup
|
15
|
+
|
16
|
+
And point your browser to `http://localhost:4567/`.
|
17
|
+
|
18
|
+
##Usage
|
19
|
+
Create new pages through accessing `http://localhost:4567/path/to/new/page.html`.
|
20
|
+
Edit existent page through accessing `http://localhost:4567/existent/page.html.e` or just press `ctrl-shift-e`.
|
21
|
+
|
22
|
+
[sinatra]: http://sinatrarb.com/
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
Jeweler::Tasks.new do |gemspec|
|
10
|
+
gemspec.name = "ciridiri"
|
11
|
+
gemspec.version = "0.8.1"
|
12
|
+
gemspec.summary = gemspec.description = "Dead simple wiki engine"
|
13
|
+
gemspec.email = "vasily@polovnyov.ru"
|
14
|
+
gemspec.homepage = "http://vast.github.com/ciridiri.rb"
|
15
|
+
gemspec.authors = ["Vasily Polovnyov"]
|
16
|
+
|
17
|
+
gemspec.add_dependency 'sinatra', '>=0.9.1'
|
18
|
+
|
19
|
+
gemspec.add_development_dependency 'rack-test', '>=0.3.0'
|
20
|
+
gemspec.add_development_dependency 'contest', '>=0.1.0'
|
21
|
+
|
22
|
+
gemspec.test_files = Dir.glob('test/*')
|
23
|
+
gemspec.files = ["LICENSE", "README.md", "Rakefile", "config.ru"] + Dir.glob('lib/**/*') + gemspec.test_files +
|
24
|
+
Dir.glob('public/**/*') + Dir.glob('views/*')
|
25
|
+
|
26
|
+
end
|
27
|
+
rescue LoadError
|
28
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
29
|
+
end
|
30
|
+
|
31
|
+
Rake::TestTask.new do |t|
|
32
|
+
t.libs << "test"
|
33
|
+
t.test_files = FileList['test/*_test.rb']
|
34
|
+
t.verbose = true
|
35
|
+
ENV['RACK_ENV'] = 'test'
|
36
|
+
end
|
data/config.ru
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift File.expand_path("#{File.dirname(__FILE__)}/lib")
|
2
|
+
require 'rubygems'
|
3
|
+
require 'sinatra'
|
4
|
+
require 'ciridiri'
|
5
|
+
|
6
|
+
# require 'rdiscount'
|
7
|
+
# Ciridiri::Page.formatter = lambda {|text| RDiscount.new(text).to_html}
|
8
|
+
|
9
|
+
set :raise_errors, true
|
10
|
+
set :show_exceptions, false
|
11
|
+
set :logging, false
|
12
|
+
|
13
|
+
run Ciridiri::Application
|
data/lib/ciridiri.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__))
|
5
|
+
require 'ciridiri/page'
|
6
|
+
|
7
|
+
class Ciridiri::Application < Sinatra::Base
|
8
|
+
include Ciridiri
|
9
|
+
configure do
|
10
|
+
set :app_file, __FILE__
|
11
|
+
set :root, File.expand_path('..', File.dirname(__FILE__))
|
12
|
+
enable :static
|
13
|
+
enable :logging if development?
|
14
|
+
|
15
|
+
Page.caching = false if development? || test?
|
16
|
+
Page.content_dir = File.join(self.root, "pages", self.environment.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
helpers do
|
20
|
+
include Rack::Utils
|
21
|
+
alias_method :h, :escape_html
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/' do
|
25
|
+
redirect '/index.html'
|
26
|
+
end
|
27
|
+
|
28
|
+
get '*.html' do
|
29
|
+
uri = params[:splat].first
|
30
|
+
if @page = Page.find_by_uri(uri)
|
31
|
+
erb :show
|
32
|
+
else
|
33
|
+
redirect "#{uri}.html.e"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
get '*.html.e' do
|
38
|
+
@page = Page.find_by_uri_or_empty(params[:splat].first)
|
39
|
+
erb :edit
|
40
|
+
end
|
41
|
+
|
42
|
+
post '*.html' do
|
43
|
+
@page = Page.find_by_uri_or_empty(params[:splat].first)
|
44
|
+
@page.contents = params[:contents]
|
45
|
+
@page.save
|
46
|
+
redirect "#{@page.uri}.html"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Extends the class object with class and instance accessors for class attributes,
|
2
|
+
# just like the native attr* accessors for instance attributes.
|
3
|
+
#
|
4
|
+
# class Person
|
5
|
+
# cattr_accessor :hair_colors
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
9
|
+
#
|
10
|
+
class Class
|
11
|
+
def cattr_reader(*syms)
|
12
|
+
syms.flatten.each do |sym|
|
13
|
+
next if sym.is_a?(Hash)
|
14
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
15
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
16
|
+
@@#{sym} = nil # @@hair_colors = nil
|
17
|
+
end # end
|
18
|
+
#
|
19
|
+
def self.#{sym} # def self.hair_colors
|
20
|
+
@@#{sym} # @@hair_colors
|
21
|
+
end # end
|
22
|
+
#
|
23
|
+
def self.#{sym}? # def self.hair_colors?
|
24
|
+
@@#{sym} # @@hair_colors
|
25
|
+
end # end
|
26
|
+
EOS
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def cattr_writer(*syms)
|
31
|
+
syms.flatten.each do |sym|
|
32
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
33
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
34
|
+
@@#{sym} = nil # @@hair_colors = nil
|
35
|
+
end # end
|
36
|
+
#
|
37
|
+
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
38
|
+
@@#{sym} = obj # @@hair_colors = obj
|
39
|
+
end # end
|
40
|
+
EOS
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def cattr_accessor(*syms)
|
45
|
+
cattr_reader(*syms)
|
46
|
+
cattr_writer(*syms)
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ciridiri
|
2
|
+
module Finders
|
3
|
+
# Convert `uri` to a path and return a new `Page` instance if the corresponding file exists.
|
4
|
+
# Return nil otherwise
|
5
|
+
def find_by_uri(uri)
|
6
|
+
content_path = path_from_uri(uri)
|
7
|
+
File.exists?(content_path) ? Page.new(uri, File.open(content_path).read) : nil
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return a new empty `Page` instance if the corresponding file doesn't exists
|
11
|
+
def find_by_uri_or_empty(uri)
|
12
|
+
find_by_uri(uri) or Page.new(uri, '')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return an array of all `Page`s excluding backups
|
16
|
+
def all
|
17
|
+
Dir.chdir(content_dir) do
|
18
|
+
files = Dir.glob(File.join("**", "*#{SOURCE_FILE_EXT}")).delete_if {|p| p =~ Regexp.new("\\.[0-9]+\\#{SOURCE_FILE_EXT}$")}
|
19
|
+
files.collect {|f| Page.new(uri_from_path(f), File.open(f, 'r') {|b| b.read})}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__)) unless $LOAD_PATH.include?(File.dirname(__FILE__))
|
2
|
+
%w[fileutils finders paths extensions].each {|r| require r}
|
3
|
+
|
4
|
+
module Ciridiri
|
5
|
+
class Page
|
6
|
+
extend Ciridiri::Finders, Ciridiri::Paths
|
7
|
+
###Constants block
|
8
|
+
|
9
|
+
# A regular expression for markdown like headers (`#`, `##`, `###`, `=====`, `----`)
|
10
|
+
MD_TITLE = Regexp.new("(^\#{1,3}\\s*?([^#].*?)#*$)|(^ {0,3}(\\S.*?)\\n(?:=|-)+(?=\\n+|\\Z))", Regexp::MULTILINE)
|
11
|
+
# HTML headers (`<h1-3>`)
|
12
|
+
HTML_TITLE = Regexp.new("^<h[1-3](.*)?>(.*)+</h[1-3]>")
|
13
|
+
|
14
|
+
# File extensions
|
15
|
+
SOURCE_FILE_EXT = ".text".freeze
|
16
|
+
CACHED_FILE_EXT = ".html".freeze
|
17
|
+
|
18
|
+
###Default values for all options
|
19
|
+
|
20
|
+
# Where pages should be stored on a file system
|
21
|
+
@@content_dir = '.'
|
22
|
+
# Should we create backups (`filename.1278278364.text`, where `1278278364` -- current timestamp) or not.
|
23
|
+
# Useful when you are not going to place `@@content_dir` under version control
|
24
|
+
@@backups = false
|
25
|
+
# Page fragments (formatted file `contents`) caching
|
26
|
+
@@caching = true
|
27
|
+
|
28
|
+
####Formatter block
|
29
|
+
|
30
|
+
# You can use any formatter. For example:
|
31
|
+
#
|
32
|
+
# Bluecloth:
|
33
|
+
# require 'bluecloth'
|
34
|
+
# Page.formatter = lambda {|text| Bluecloth.new(text).to_html)}
|
35
|
+
#
|
36
|
+
# RDiscount:
|
37
|
+
# require 'rdiscount'
|
38
|
+
# Page.formatter = lambda {|text| RDiscount.new(text).to_html)}
|
39
|
+
#
|
40
|
+
# Rutils with RDiscount:
|
41
|
+
# require 'rutils'
|
42
|
+
# require 'rdiscount'
|
43
|
+
# Page.formatter = lambda {|text| RuTils::Gilenson::Formatter.new(RDiscount.new(text).to_html).to_html}
|
44
|
+
#
|
45
|
+
# HTML escaping:
|
46
|
+
# Page.formatter = {|text| "<pre>#{Rack::Utils.escape_html(text)}</pre>"}
|
47
|
+
#
|
48
|
+
@@formatter = lambda {|text| text}
|
49
|
+
|
50
|
+
# Define attr_reader/accessors
|
51
|
+
attr_accessor :title, :contents
|
52
|
+
attr_reader :path, :uri
|
53
|
+
|
54
|
+
# Class level attr_accessors. We use them for configuring: `Page.content_dir = '/tmp'`
|
55
|
+
cattr_accessor :content_dir, :backups, :caching, :formatter
|
56
|
+
|
57
|
+
###Public methods
|
58
|
+
|
59
|
+
# Convert `uri` to `path`, find the `title` in `contents`
|
60
|
+
def initialize(uri, contents)
|
61
|
+
@path, @uri, @title, @contents = Page.path_from_uri(uri), uri, Page.find_title(contents), contents
|
62
|
+
end
|
63
|
+
|
64
|
+
# Create needed directory hierarchy and backup the file if needed.
|
65
|
+
# Write `@contents` to the file and return `true` or `false` if
|
66
|
+
# any error occured
|
67
|
+
def save
|
68
|
+
FileUtils.mkdir_p(File.dirname(@path)) unless File.exists?(@path)
|
69
|
+
backup if Page.backups? && File.exists?(@path)
|
70
|
+
|
71
|
+
begin
|
72
|
+
File.open(@path, "w") {|f| f.write(@contents)}
|
73
|
+
true
|
74
|
+
rescue StandardError
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Save `@contents` formatted with `Page.formatter` to the cache file
|
80
|
+
# `index.text` -> `index.text.html`
|
81
|
+
def cache!
|
82
|
+
File.open(@path + CACHED_FILE_EXT, 'w') {|f| f.write(@@formatter.call(@contents))}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Delete the cache file
|
86
|
+
def sweep!
|
87
|
+
File.delete(@path + CACHED_FILE_EXT)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Return an array of the page revisions
|
91
|
+
def revisions
|
92
|
+
@revisions ||= find_revisions
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return `@contents` HTML representation.
|
96
|
+
# If a page fragments caching enabled (`Page.caching = true`) then
|
97
|
+
# regenerate the fragment cache (`index.text.html`) if needed (it's outdated or doesn't exist)
|
98
|
+
# and return the cached contents.
|
99
|
+
# Otherwise (`Page.caching = false`) return `@contents` formatted with `Page.formatter`
|
100
|
+
def to_html
|
101
|
+
if Page.caching?
|
102
|
+
cached = @path + CACHED_FILE_EXT
|
103
|
+
cache! if !File.exists?(cached) || File.mtime(@path) > File.mtime(cached)
|
104
|
+
|
105
|
+
File.open(cached).read
|
106
|
+
else
|
107
|
+
@@formatter.call(@contents)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Tiny `attr_writer` for `@@content_dir` which creates the content directory if it doesn't exist
|
112
|
+
def self.content_dir=(dir)
|
113
|
+
@@content_dir = dir
|
114
|
+
FileUtils.mkdir_p(@@content_dir) if !File.exists?(@@content_dir)
|
115
|
+
end
|
116
|
+
|
117
|
+
###Protected methods
|
118
|
+
|
119
|
+
protected
|
120
|
+
# Find the title in contents (html or markdown variant).
|
121
|
+
# Return `""` if nothing found.
|
122
|
+
def self.find_title(contents="")
|
123
|
+
if contents.detect {|s| s.match(MD_TITLE)}
|
124
|
+
$2.strip || $4.strip
|
125
|
+
elsif contents.detect {|s| s.match(HTML_TITLE)}
|
126
|
+
$2.strip
|
127
|
+
else
|
128
|
+
""
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Collect only timestamps of revisions.
|
133
|
+
# `index.1273434670.text`, `index.1273434450.text` -> `["1273434670", "1273434450"]`
|
134
|
+
def find_revisions
|
135
|
+
Dir.chdir(File.dirname(@path)) do
|
136
|
+
basename = File.basename(@path, SOURCE_FILE_EXT)
|
137
|
+
Dir.glob(basename + ".*" + SOURCE_FILE_EXT).
|
138
|
+
collect {|f| File.basename(f, SOURCE_FILE_EXT).sub(basename, '')}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Backup the file by copying it's current version to the same file but with the current timestamp.
|
143
|
+
# `index.text` -> `index.1273434670.text`
|
144
|
+
def backup
|
145
|
+
FileUtils.cp(@path, @path.sub(Regexp.new("#{SOURCE_FILE_EXT}$"), ".#{Time.now.to_i.to_s}#{SOURCE_FILE_EXT}"))
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Ciridiri
|
2
|
+
module Paths
|
3
|
+
# Convert `uri` to `path` in a file system including `content_dir` and a source file extension
|
4
|
+
# `/team/pro/chuck-norris` -> `content_dir/team/pro/chuck-norris.text`
|
5
|
+
def path_from_uri(uri)
|
6
|
+
path = uri.split("/")
|
7
|
+
filename = path.pop
|
8
|
+
File.join(content_dir, path, "#{filename}#{Page::SOURCE_FILE_EXT}")
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
data/public/css/base.css
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
/*
|
2
|
+
based on:
|
3
|
+
+----------------------------------------------------------------------------------------------------+
|
4
|
+
| |
|
5
|
+
| TYPOGRIDPHY - TYPOGRAPHICAL AND GRID LAYOUT CSS FRAMEWORK FROM HARRY ROBERTS OF CSS WIZARDRY |
|
6
|
+
| |
|
7
|
+
+-------------------------------------------------+--------------------------------------------------+
|
8
|
+
| | |
|
9
|
+
| TYPOGRIDPHY IS © COPYRIGHT OF HARRY ROBERTS | v 0.1.1 |
|
10
|
+
| IT IS FREE TO BE USED AND MODIFIED PROVIDED | May 2008 |
|
11
|
+
| THIS TEXT REMAINS INTACT -- CSSWIZARDRY.COM | http://csswizardry.com |
|
12
|
+
| | |
|
13
|
+
+-------------------------------------------------+--------------------------------------------------+
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
COLOUR REFERENCES
|
18
|
+
BODY BG: #FFF
|
19
|
+
TOP STRIP: #000
|
20
|
+
BODY COLOUR: #444
|
21
|
+
LINKS: #000
|
22
|
+
-------------------------------------------------------- */
|
23
|
+
|
24
|
+
|
25
|
+
/* RESET */
|
26
|
+
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
|
27
|
+
margin: 0;
|
28
|
+
padding: 0
|
29
|
+
}
|
30
|
+
table {
|
31
|
+
border-collapse: collapse;
|
32
|
+
border-spacing: 0
|
33
|
+
}
|
34
|
+
fieldset,img {
|
35
|
+
border: 0
|
36
|
+
}
|
37
|
+
address,caption,cite,code,dfn,em,strong,th,var {
|
38
|
+
font-style: normal;
|
39
|
+
font-weight: normal
|
40
|
+
}
|
41
|
+
ol,ul {
|
42
|
+
list-style: none
|
43
|
+
}
|
44
|
+
caption,th {
|
45
|
+
text-align: left
|
46
|
+
}
|
47
|
+
h1,h2,h3,h4,h5,h6 {
|
48
|
+
font-size: 100%;
|
49
|
+
font-weight: normal
|
50
|
+
}
|
51
|
+
q:before,q:after {
|
52
|
+
content:''
|
53
|
+
}
|
54
|
+
abbr,acronym {
|
55
|
+
border:0
|
56
|
+
}
|
57
|
+
/*---------- END RESET ----------*/
|
58
|
+
|
59
|
+
|
60
|
+
/*-------------------------------------------+
|
61
|
+
| |
|
62
|
+
| MAIN STRUCTURE STYLES |
|
63
|
+
| |
|
64
|
+
+-------------------------------------------*/
|
65
|
+
html {
|
66
|
+
font-size: 100%;
|
67
|
+
min-height: 101%
|
68
|
+
}
|
69
|
+
body {
|
70
|
+
font-family: Georgia, "Times New Roman", serif;
|
71
|
+
color: #444;
|
72
|
+
background: #fff;
|
73
|
+
border-top: .5em solid #444;
|
74
|
+
padding: 0 1em
|
75
|
+
}
|
76
|
+
#wrap {
|
77
|
+
width: 940px;
|
78
|
+
margin: 0 auto
|
79
|
+
}
|
80
|
+
#header {
|
81
|
+
padding-top: 1.5em;
|
82
|
+
margin: 0 0 2em
|
83
|
+
}
|
84
|
+
/*-------------------------------------------+
|
85
|
+
| |
|
86
|
+
| FONT STYLES |
|
87
|
+
| |
|
88
|
+
+-------------------------------------------*/
|
89
|
+
/*PARAGRAPHS
|
90
|
+
-------------------------------------------------------- */
|
91
|
+
p, pre {
|
92
|
+
line-height: 1.5em;
|
93
|
+
margin: 0 0 1.5em
|
94
|
+
}
|
95
|
+
/* Styles an introductory paragraph, similar to newspapers. Assign this class to the first paragraph in an article */
|
96
|
+
p.intro:first-line {
|
97
|
+
font-variant: small-caps
|
98
|
+
}
|
99
|
+
/* Styles a drop cap on each paragraph with this class */
|
100
|
+
p.drop:first-letter {
|
101
|
+
float: left;
|
102
|
+
font-size: 3em;
|
103
|
+
margin: -.05em .1em -.5em 0
|
104
|
+
}
|
105
|
+
/*HEADINGS
|
106
|
+
-------------------------------------------------------- */
|
107
|
+
h1, h2, h3, h4, h5, h6 {margin-bottom: .5em; color: #000}
|
108
|
+
h3, h4, h5 {font-variant: small-caps}
|
109
|
+
h1 {
|
110
|
+
font-size: 3em;
|
111
|
+
margin-top: .6em;
|
112
|
+
font-style: italic;
|
113
|
+
line-height: 1.2em
|
114
|
+
}
|
115
|
+
h2 {
|
116
|
+
font-size: 2em;
|
117
|
+
margin-top: .9em;
|
118
|
+
line-height: .9em
|
119
|
+
}
|
120
|
+
h3 {
|
121
|
+
font-size: 1.5em;
|
122
|
+
margin-top: 1.2em;
|
123
|
+
line-height: 1.2em
|
124
|
+
}
|
125
|
+
h4 {
|
126
|
+
font-size: 1.2em;
|
127
|
+
margin-top: 1.5em;
|
128
|
+
line-height: 1.5em
|
129
|
+
}
|
130
|
+
h5 {
|
131
|
+
font-size: 1em;
|
132
|
+
margin-top: 1.8em;
|
133
|
+
line-height: 1.8em
|
134
|
+
}
|
135
|
+
h6 {
|
136
|
+
font-size: 1em;
|
137
|
+
margin-top: 1.8em;
|
138
|
+
line-height: 1.8em
|
139
|
+
}
|
140
|
+
/*LINKS
|
141
|
+
-------------------------------------------------------- */
|
142
|
+
a {color: #000}
|
143
|
+
a:hover {text-decoration: none}
|
144
|
+
/*ALL THE TRIMMINGS
|
145
|
+
-------------------------------------------------------- */
|
146
|
+
blockquote p {
|
147
|
+
font-size: 1.2em!important;
|
148
|
+
line-height: 1.5em!important;
|
149
|
+
margin-bottom: 1.5em!important;
|
150
|
+
font-style: italic;
|
151
|
+
font-weight: bold
|
152
|
+
}
|
153
|
+
blockquote p cite {font-style: normal}
|
154
|
+
strong {font-variant:small-caps}
|
155
|
+
em {
|
156
|
+
font-style: italic;
|
157
|
+
font-weight: inherit
|
158
|
+
}
|
159
|
+
.amp { /* Give those ampersands a right sexy look */
|
160
|
+
font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", serif;
|
161
|
+
font-style: italic;
|
162
|
+
font-weight: normal;
|
163
|
+
line-height: inherit
|
164
|
+
}
|
165
|
+
abbr {
|
166
|
+
border-bottom: 1px dotted;
|
167
|
+
cursor: help
|
168
|
+
}
|
169
|
+
.clear {clear:both}
|
170
|
+
.right-float { /* Float any item to the right */
|
171
|
+
float: right;
|
172
|
+
margin: 0 0 0 2em
|
173
|
+
}
|
174
|
+
.left-float { /* Float any item to the left */
|
175
|
+
float:left;
|
176
|
+
margin: 0 2em 0 0
|
177
|
+
}
|
178
|
+
pre, code { /* Styling for and code type items */
|
179
|
+
font-family: consolas, lucida console, bitstream vera sans mono, courier new, monospace;
|
180
|
+
font-size: 87.5%;
|
181
|
+
color: #007A00
|
182
|
+
}
|
183
|
+
pre code {font-size: 1em}
|
184
|
+
/*-------------------------------------------+
|
185
|
+
| |
|
186
|
+
| IMAGE STYLES |
|
187
|
+
| |
|
188
|
+
+-------------------------------------------*/
|
189
|
+
img {font-size: 1em}
|
190
|
+
img.left-img { /* Float any image to the LEFT and give it some margin */
|
191
|
+
float: left;
|
192
|
+
padding: 4px;
|
193
|
+
border: 1px solid #ccc;
|
194
|
+
margin: .3em 2em 1.8em 0
|
195
|
+
}
|
196
|
+
img.right-img { /* Float any image to the RIGHT and give it some margin */
|
197
|
+
float: right;
|
198
|
+
padding: 4px;
|
199
|
+
border: 1px solid #ccc;
|
200
|
+
margin: .3em 0 1.8em 2em
|
201
|
+
}
|
202
|
+
/*-------------------------------------------+
|
203
|
+
| |
|
204
|
+
| LIST STYLES |
|
205
|
+
| |
|
206
|
+
+-------------------------------------------*/
|
207
|
+
ul {
|
208
|
+
margin-bottom: 1.8em;
|
209
|
+
list-style: square inside;
|
210
|
+
}
|
211
|
+
ul li {line-height:1.5em}
|
212
|
+
ul li.caption { /* Apply this class to the first list item in a list to give it a caption */
|
213
|
+
font-variant: small-caps;
|
214
|
+
list-style: none;
|
215
|
+
color: #000
|
216
|
+
}
|
217
|
+
li > ul, li > ol {
|
218
|
+
margin-bottom: 0;
|
219
|
+
margin-left: 5em
|
220
|
+
}
|
221
|
+
li > ul li, li > ol li {font-size: 1em}
|
222
|
+
ol {
|
223
|
+
margin-bottom: 1.8em;
|
224
|
+
list-style: decimal inside
|
225
|
+
}
|
226
|
+
ol li {line-height:1.5em}
|
227
|
+
/*-------------------------------------------+
|
228
|
+
| |
|
229
|
+
| MISC. STYLES |
|
230
|
+
| |
|
231
|
+
+-------------------------------------------*/
|
232
|
+
.submits {margin: 1em 0}
|
233
|
+
.submits input {font-size: 112%}
|
234
|
+
|
235
|
+
/*
|
236
|
+
|
237
|
+
"I could eat a knob at night"
|
238
|
+
- Karl Pilkington
|
239
|
+
|
240
|
+
*/
|
@@ -0,0 +1,31 @@
|
|
1
|
+
body {
|
2
|
+
margin: 0;
|
3
|
+
padding: 0;
|
4
|
+
color: #252525;
|
5
|
+
font: normal 12pt/1.5 Cambria, Georgia, serif
|
6
|
+
}
|
7
|
+
|
8
|
+
a {text-decoration: none; color: #252525}
|
9
|
+
a img {border: 0}
|
10
|
+
h1, h2, h3, h4 {
|
11
|
+
margin: .8em 0 8px;
|
12
|
+
font-weight: normal;
|
13
|
+
color: #000;
|
14
|
+
line-height: 1.2
|
15
|
+
}
|
16
|
+
h1 {font-size: 250%}
|
17
|
+
h2 {font-size: 200%}
|
18
|
+
h3 {font-size: 150%}
|
19
|
+
h4 {font-size: 125%}
|
20
|
+
h5 {font-size: 110%}
|
21
|
+
p {margin: 0 0 .6em}
|
22
|
+
blockquote {font-style: italic}
|
23
|
+
pre, code {
|
24
|
+
font-family: monaco, lucida console, bitstream vera sans mono, monospace;
|
25
|
+
font-size: 10pt
|
26
|
+
}
|
27
|
+
h3+blockquote, h2+blockquote, p+ul, h2+ul, h3+ul {margin-top: .2em}
|
28
|
+
|
29
|
+
.noprint, form {
|
30
|
+
display: none
|
31
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
document.onkeydown = NavigateThrough;
|
2
|
+
|
3
|
+
function NavigateThrough (event) {
|
4
|
+
if (!document.getElementById) return;
|
5
|
+
if (window.event) event = window.event;
|
6
|
+
if (event.ctrlKey && event.shiftKey) {
|
7
|
+
var link = null;
|
8
|
+
var href = null;
|
9
|
+
switch (event.keyCode ? event.keyCode: event.which ? event.which: null) {
|
10
|
+
case 0x45:
|
11
|
+
link = document.getElementById ('edit-link');
|
12
|
+
break;
|
13
|
+
}
|
14
|
+
|
15
|
+
if (link && link.href) document.location = link.href;
|
16
|
+
if (href) document.location = href;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
function ctrlEnterSubmit(e, form) {
|
21
|
+
if (((e.keyCode == 13) || (e.keyCode == 10)) && (e.ctrlKey == true)) form.submit();
|
22
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'rack/test'
|
3
|
+
|
4
|
+
class CiridiriTest < Test::Unit::TestCase
|
5
|
+
include Rack::Test::Methods
|
6
|
+
describe "Ciridiri web-app" do
|
7
|
+
def app
|
8
|
+
Ciridiri::Application
|
9
|
+
end
|
10
|
+
|
11
|
+
should "redirect from root to index" do
|
12
|
+
get '/'
|
13
|
+
assert_redirect('/index.html')
|
14
|
+
end
|
15
|
+
|
16
|
+
should "get existent page" do
|
17
|
+
page = page_stub
|
18
|
+
page.save
|
19
|
+
|
20
|
+
get "#{page.uri}.html"
|
21
|
+
assert last_response.ok?
|
22
|
+
assert last_response.body.include?(page.title)
|
23
|
+
end
|
24
|
+
|
25
|
+
should "redirect to edit form if page not found" do
|
26
|
+
get "/nonexistent.html"
|
27
|
+
assert_redirect("/nonexistent.html.e")
|
28
|
+
end
|
29
|
+
|
30
|
+
should "show an empty edit form" do
|
31
|
+
get "/nonexistent.html.e"
|
32
|
+
assert last_response.ok?
|
33
|
+
assert last_response.body.include?("form")
|
34
|
+
end
|
35
|
+
|
36
|
+
should "create a new page" do
|
37
|
+
post "/foo.html", :contents => 'fut-fut-fut, freeeeestylo'
|
38
|
+
assert_redirect("/foo.html")
|
39
|
+
follow_redirect!
|
40
|
+
assert last_response.ok?
|
41
|
+
assert last_response.body.include?('freeeeestylo')
|
42
|
+
end
|
43
|
+
|
44
|
+
should "edit an existent page" do
|
45
|
+
page = page_stub
|
46
|
+
page.save
|
47
|
+
|
48
|
+
get "#{page.uri}.html.e"
|
49
|
+
assert last_response.ok?
|
50
|
+
assert last_response.body.include?("textarea")
|
51
|
+
assert last_response.body.include?(page.contents)
|
52
|
+
end
|
53
|
+
|
54
|
+
should "update an existent page" do
|
55
|
+
page = page_stub
|
56
|
+
page.save
|
57
|
+
|
58
|
+
post "#{page.uri}.html", :contents => "new contents"
|
59
|
+
assert_redirect("#{page.uri}.html")
|
60
|
+
follow_redirect!
|
61
|
+
assert last_response.ok?
|
62
|
+
assert last_response.body.include?("new contents")
|
63
|
+
end
|
64
|
+
|
65
|
+
should "provide the edit-link" do
|
66
|
+
page = page_stub
|
67
|
+
page.save
|
68
|
+
|
69
|
+
get "#{page.uri}.html"
|
70
|
+
assert last_response.ok?
|
71
|
+
assert last_response.body.include?("edit-link")
|
72
|
+
assert last_response.body.include?("#{page.uri}.html.e")
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
def assert_redirect(uri)
|
77
|
+
assert last_response.redirect?
|
78
|
+
assert_equal last_response.headers['Location'], uri
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/test/page_test.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class PageTest < Test::Unit::TestCase
|
4
|
+
describe "Ciridiri::Page" do
|
5
|
+
should "create correct page" do
|
6
|
+
@page = page_stub
|
7
|
+
assert_not_nil @page.contents
|
8
|
+
assert_not_nil @page.title
|
9
|
+
assert_equal 'awesome title', @page.title
|
10
|
+
end
|
11
|
+
|
12
|
+
should "save page" do
|
13
|
+
@page = page_stub
|
14
|
+
assert @page.save
|
15
|
+
assert File.exists?(@page.path)
|
16
|
+
assert File.size(@page.path) > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
should "parse html and md titles" do
|
20
|
+
@page = page_stub
|
21
|
+
@else_one_page = page_stub("index", "<h3 class=\"title\">awesome title</h3>\n hello, everyone!")
|
22
|
+
assert_equal @page.title, "awesome title"
|
23
|
+
assert_equal @page.title, @else_one_page.title
|
24
|
+
end
|
25
|
+
|
26
|
+
should "update page" do
|
27
|
+
@page = page_stub
|
28
|
+
@page.save
|
29
|
+
@page.contents = "#new title\nand new content"
|
30
|
+
assert @page.save
|
31
|
+
assert File.open(@page.path).read.include?('and new content')
|
32
|
+
end
|
33
|
+
|
34
|
+
should "find page by uri" do
|
35
|
+
@page = page_stub("hidden/blah")
|
36
|
+
@page.save
|
37
|
+
|
38
|
+
@p = Page.find_by_uri('hidden/blah')
|
39
|
+
assert_not_nil @p
|
40
|
+
assert_not_nil @p.title
|
41
|
+
assert_not_nil @p.contents
|
42
|
+
end
|
43
|
+
|
44
|
+
should "return nil if page is not found" do
|
45
|
+
assert_nil Page.find_by_uri('nonexistent-uri')
|
46
|
+
end
|
47
|
+
|
48
|
+
should "return empty page if needed" do
|
49
|
+
assert_not_nil Page.find_by_uri_or_empty('nonexistent-uri')
|
50
|
+
end
|
51
|
+
|
52
|
+
should "respect uri hierarchy" do
|
53
|
+
@page = page_stub('about/team/boris')
|
54
|
+
@page.save
|
55
|
+
|
56
|
+
target_path = File.expand_path(File.join(Page.content_dir, %w[about team], "boris#{Page::SOURCE_FILE_EXT}"))
|
57
|
+
assert File.exists?(target_path)
|
58
|
+
assert_equal target_path, File.expand_path(@page.path)
|
59
|
+
end
|
60
|
+
|
61
|
+
should "create backups if needed" do
|
62
|
+
begin
|
63
|
+
Page.backups = true
|
64
|
+
@page = page_stub
|
65
|
+
assert @page.save
|
66
|
+
@page.contents = "foo bar"
|
67
|
+
assert @page.save
|
68
|
+
|
69
|
+
assert_not_nil @page.revisions
|
70
|
+
assert_equal @page.revisions.length, 1
|
71
|
+
ensure
|
72
|
+
Page.backups = false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
should "format contents" do
|
77
|
+
begin
|
78
|
+
@page = page_stub
|
79
|
+
@page.save
|
80
|
+
assert_equal @page.contents, @page.to_html
|
81
|
+
Page.formatter = lambda {|t| "<censored />"}
|
82
|
+
assert_equal @page.to_html, "<censored />"
|
83
|
+
ensure
|
84
|
+
Page.formatter = lambda {|t| t}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
should "cache page" do
|
89
|
+
begin
|
90
|
+
Page.caching = true
|
91
|
+
@page = page_stub
|
92
|
+
@page.save
|
93
|
+
assert_not_nil @page.to_html
|
94
|
+
assert File.exists?(@page.path + Page::CACHED_FILE_EXT)
|
95
|
+
assert File.size(@page.path + Page::CACHED_FILE_EXT) > 0
|
96
|
+
ensure
|
97
|
+
Page.caching = false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'contest'
|
4
|
+
require 'lib/ciridiri'
|
5
|
+
begin; require 'turn'; rescue LoadError; end
|
6
|
+
|
7
|
+
Ciridiri::Page.content_dir = File.join(File.dirname(__FILE__), 'pages')
|
8
|
+
Ciridiri::Page.caching = false
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
include Ciridiri
|
12
|
+
|
13
|
+
class << self
|
14
|
+
alias_method :it, :test
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
#recreate an empty content directory
|
19
|
+
FileUtils.rm_rf(Page.content_dir)
|
20
|
+
FileUtils.mkdir(Page.content_dir)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def page_stub(uri = '/index', body = "##awesome title\n hello, everyone!")
|
25
|
+
Page.new(uri, body)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/views/edit.erb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
<h1>Edit page</h1>
|
2
|
+
<script type="text/javascript">
|
3
|
+
window.onload = function() {
|
4
|
+
document.getElementById('p-contents').focus();
|
5
|
+
}
|
6
|
+
</script>
|
7
|
+
<form action="<%= @page.uri %>.html" method="post" onkeypress="ctrlEnterSubmit(event, this)">
|
8
|
+
<fieldset>
|
9
|
+
<div>
|
10
|
+
<textarea rows="30" cols="20" id="p-contents" name="contents" style="width: 100%"><%= h @page.contents %></textarea>
|
11
|
+
</div>
|
12
|
+
</fieldset>
|
13
|
+
<fieldset class="submits">
|
14
|
+
<input type="submit" value="Save" /> or <a href="<%= @page.uri %>.html">cancel</a>
|
15
|
+
</fieldset>
|
16
|
+
</form>
|
data/views/layout.erb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
5
|
+
<link rel="stylesheet" media="screen, projection" href="/css/base.css" />
|
6
|
+
<link rel="stylesheet" media="print" href="/css/print.css" />
|
7
|
+
<% if @page && !(request.fullpath =~ /\.e$/) %>
|
8
|
+
<link rel="alternate" id="edit-link" type="application/x-wiki" title="Edit current page" href="<%= @page.uri %>.html.e" />
|
9
|
+
<% end %>
|
10
|
+
<% if request.fullpath =~ /\.e$/ %>
|
11
|
+
<meta name="robots" content="noindex" />
|
12
|
+
<% end %>
|
13
|
+
<script src="/js/application.js"></script>
|
14
|
+
<title>
|
15
|
+
<%= h @page.title + " / " if @page && @page.title%>
|
16
|
+
ciridiri.rb: dead simple wiki engine
|
17
|
+
</title>
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<div id="wrap">
|
21
|
+
<div id="page">
|
22
|
+
<%= yield %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
</body>
|
26
|
+
</html>
|
data/views/show.erb
ADDED
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ciridiri
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 61
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 8
|
9
|
+
- 1
|
10
|
+
version: 0.8.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Vasily Polovnyov
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-01 00:00:00 +04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: sinatra
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 57
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 9
|
33
|
+
- 1
|
34
|
+
version: 0.9.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rack-test
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 19
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 3
|
49
|
+
- 0
|
50
|
+
version: 0.3.0
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: contest
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 27
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 1
|
65
|
+
- 0
|
66
|
+
version: 0.1.0
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description: Dead simple wiki engine
|
70
|
+
email: vasily@polovnyov.ru
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files:
|
76
|
+
- README.md
|
77
|
+
files:
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- config.ru
|
81
|
+
- lib/ciridiri.rb
|
82
|
+
- lib/ciridiri/extensions.rb
|
83
|
+
- lib/ciridiri/finders.rb
|
84
|
+
- lib/ciridiri/page.rb
|
85
|
+
- lib/ciridiri/paths.rb
|
86
|
+
- public/css/base.css
|
87
|
+
- public/css/print.css
|
88
|
+
- public/js/application.js
|
89
|
+
- test/ciridiri_test.rb
|
90
|
+
- test/page_test.rb
|
91
|
+
- test/test_helper.rb
|
92
|
+
- views/edit.erb
|
93
|
+
- views/layout.erb
|
94
|
+
- views/show.erb
|
95
|
+
has_rdoc: true
|
96
|
+
homepage: http://vast.github.com/ciridiri.rb
|
97
|
+
licenses: []
|
98
|
+
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options:
|
101
|
+
- --charset=UTF-8
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
hash: 3
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
version: "0"
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
hash: 3
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
version: "0"
|
122
|
+
requirements: []
|
123
|
+
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.3.7
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: Dead simple wiki engine
|
129
|
+
test_files:
|
130
|
+
- test/test_helper.rb
|
131
|
+
- test/ciridiri_test.rb
|
132
|
+
- test/page_test.rb
|