wiki 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/wiki.rb +2 -0
- data/lib/wiki/ReadMe.md +89 -0
- data/lib/wiki/config.ru +2 -0
- data/lib/wiki/favicon.rb +31 -0
- data/lib/wiki/page.rb +74 -0
- data/lib/wiki/random_id.rb +5 -0
- data/lib/wiki/server.rb +336 -0
- data/lib/wiki/server_helpers.rb +66 -0
- data/lib/wiki/stores/ReadMe.md +26 -0
- data/lib/wiki/stores/all.rb +3 -0
- data/lib/wiki/stores/couch.rb +121 -0
- data/lib/wiki/stores/file.rb +53 -0
- data/lib/wiki/stores/store.rb +38 -0
- data/lib/wiki/version.rb +3 -0
- data/lib/wiki/views/client/Gruntfile.js +50 -0
- data/lib/wiki/views/client/ReadMe.md +67 -0
- data/lib/wiki/views/client/build-test.bat +10 -0
- data/lib/wiki/views/client/build.bat +8 -0
- data/lib/wiki/views/client/builder.pl +41 -0
- data/lib/wiki/views/client/client.coffee +3 -0
- data/lib/wiki/views/client/client.js +3607 -0
- data/lib/wiki/views/client/crosses.png +0 -0
- data/lib/wiki/views/client/images/external-link-ltr-icon.png +0 -0
- data/lib/wiki/views/client/images/noise.png +0 -0
- data/lib/wiki/views/client/images/oops.jpg +0 -0
- data/lib/wiki/views/client/js/d3/d3.behavior.js +198 -0
- data/lib/wiki/views/client/js/d3/d3.chart.js +984 -0
- data/lib/wiki/views/client/js/d3/d3.csv.js +92 -0
- data/lib/wiki/views/client/js/d3/d3.geo.js +566 -0
- data/lib/wiki/views/client/js/d3/d3.geom.js +825 -0
- data/lib/wiki/views/client/js/d3/d3.js +3597 -0
- data/lib/wiki/views/client/js/d3/d3.layout.js +1923 -0
- data/lib/wiki/views/client/js/d3/d3.time.js +660 -0
- data/lib/wiki/views/client/js/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/lib/wiki/views/client/js/images/ui-icons_222222_256x240.png +0 -0
- data/lib/wiki/views/client/js/jquery-1.6.2.min.js +18 -0
- data/lib/wiki/views/client/js/jquery-1.7.1.min.js +4 -0
- data/lib/wiki/views/client/js/jquery-1.9.1.min.js +5 -0
- data/lib/wiki/views/client/js/jquery-migrate-1.1.1.min.js +3 -0
- data/lib/wiki/views/client/js/jquery-ui-1.10.1.custom.min.css +5 -0
- data/lib/wiki/views/client/js/jquery-ui-1.10.1.custom.min.js +6 -0
- data/lib/wiki/views/client/js/jquery-ui-1.8.16.custom.css +339 -0
- data/lib/wiki/views/client/js/jquery-ui-1.8.16.custom.min.js +315 -0
- data/lib/wiki/views/client/js/jquery.ie.cors.js +310 -0
- data/lib/wiki/views/client/js/jquery.ui.touch-punch.min.js +11 -0
- data/lib/wiki/views/client/js/modernizr.custom.63710.js +824 -0
- data/lib/wiki/views/client/js/sockjs-0.3.min.js +27 -0
- data/lib/wiki/views/client/js/underscore-min.js +30 -0
- data/lib/wiki/views/client/mkplugin.sh +97 -0
- data/lib/wiki/views/client/package.json +36 -0
- data/lib/wiki/views/client/runtests.html +26 -0
- data/lib/wiki/views/client/style.css +339 -0
- data/lib/wiki/views/client/test/mocha.css +231 -0
- data/lib/wiki/views/client/test/mocha.js +5340 -0
- data/lib/wiki/views/client/test/testclient.js +17133 -0
- data/lib/wiki/views/client/testclient.coffee +18 -0
- data/lib/wiki/views/client/theme/granite.css +59 -0
- data/lib/wiki/views/client/theme/stoneSeamless.jpg +0 -0
- data/lib/wiki/views/client/twitter-maintainance.jpg +0 -0
- data/lib/wiki/views/layout.haml +56 -0
- data/lib/wiki/views/oops.haml +5 -0
- data/lib/wiki/views/page.haml +20 -0
- data/lib/wiki/views/static.html +30 -0
- data/lib/wiki/views/view.haml +2 -0
- data/wiki.gemspec +28 -0
- metadata +121 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module ServerHelpers
|
2
|
+
|
3
|
+
def cross_origin
|
4
|
+
headers 'Access-Control-Allow-Origin' => "*" if request.env['HTTP_ORIGIN']
|
5
|
+
end
|
6
|
+
|
7
|
+
def resolve_links string
|
8
|
+
string.
|
9
|
+
gsub(/\[\[([^\]]+)\]\]/i) {
|
10
|
+
|name|
|
11
|
+
name.gsub!(/^\[\[(.*)\]\]/, '\1')
|
12
|
+
|
13
|
+
slug = name.gsub(/\s/, '-')
|
14
|
+
slug = slug.gsub(/[^A-Za-z0-9-]/, '').downcase
|
15
|
+
'<a class="internal" href="/'+slug+'.html" data-page-name="'+slug+'" >'+name+'</a>'
|
16
|
+
}.
|
17
|
+
gsub(/\[(http.*?) (.*?)\]/i, '<a class="external" href="\1" rel="nofollow">\2</a>')
|
18
|
+
end
|
19
|
+
|
20
|
+
def openid_consumer
|
21
|
+
@openid_consumer ||= OpenID::Consumer.new(session, OpenID::Store::Filesystem.new("#{farm_status}/tmp/openid"))
|
22
|
+
end
|
23
|
+
|
24
|
+
def authenticated?
|
25
|
+
session[:authenticated] == true
|
26
|
+
end
|
27
|
+
|
28
|
+
def identified?
|
29
|
+
Store.exists? "#{farm_status}/open_id.identifier"
|
30
|
+
end
|
31
|
+
|
32
|
+
def claimed?
|
33
|
+
Store.exists? "#{farm_status}/open_id.identity"
|
34
|
+
end
|
35
|
+
|
36
|
+
def authenticate!
|
37
|
+
session[:authenticated] = true
|
38
|
+
redirect "/"
|
39
|
+
end
|
40
|
+
|
41
|
+
def oops status, message
|
42
|
+
haml :oops, :layout => false, :locals => {:status => status, :message => message}
|
43
|
+
end
|
44
|
+
|
45
|
+
def serve_resources_locally?(site)
|
46
|
+
!!ENV['FARM_DOMAINS'] && ENV['FARM_DOMAINS'].split(',').any?{|domain| site.end_with?(domain)}
|
47
|
+
end
|
48
|
+
|
49
|
+
def serve_page(name, site=request.host)
|
50
|
+
cross_origin
|
51
|
+
halt 404 unless farm_page(site).exists?(name)
|
52
|
+
JSON.pretty_generate farm_page(site).get(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def synopsis page
|
56
|
+
text = page['synopsis']
|
57
|
+
p1 = page['story'] && page['story'][0]
|
58
|
+
p2 = page['story'] && page['story'][1]
|
59
|
+
text ||= p1 && p1['text'] if p1 && p1['type'] == 'paragraph'
|
60
|
+
text ||= p2 && p2['text'] if p2 && p2['type'] == 'paragraph'
|
61
|
+
text ||= p1 && p1['text'] || p2 && p2['text'] || page['story'] && "A page with #{page['story'].length} paragraphs." || "A page with no story."
|
62
|
+
return text
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
We support several wiki page persistence mechanisms called Stores.
|
2
|
+
Currently the server includes all versions and selects one with
|
3
|
+
an environment variable.
|
4
|
+
|
5
|
+
File Store
|
6
|
+
==========
|
7
|
+
|
8
|
+
Pages are stored in flat files under `data` in the subdirectory
|
9
|
+
`pages`. File names are the slugs with no suffix.
|
10
|
+
A second subdirectory, `status`, contains additional metadata
|
11
|
+
such as the site's favicon.png.
|
12
|
+
|
13
|
+
When the server is operated as a wiki site farm,
|
14
|
+
data and status subdirectories are pushed several levels deeper
|
15
|
+
in the file hierarchy under `data/farm/*` where * is replaced
|
16
|
+
with the virtual host domain name.
|
17
|
+
The existence of the farm subdirectory configures the server
|
18
|
+
into farm mode.
|
19
|
+
|
20
|
+
Couch Store
|
21
|
+
===========
|
22
|
+
|
23
|
+
Pages are stored as Couch documents with fully qualified
|
24
|
+
names following the conventions established in the File Store.
|
25
|
+
An environment variable indicates that the server should
|
26
|
+
be in farm mode.
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'time' # for Time#iso8601
|
2
|
+
|
3
|
+
class CouchStore < Store
|
4
|
+
class << self
|
5
|
+
|
6
|
+
attr_writer :db # used by specs
|
7
|
+
|
8
|
+
def db
|
9
|
+
unless @db
|
10
|
+
couchdb_server = ENV['COUCHDB_URL'] || raise('please set ENV["COUCHDB_URL"]')
|
11
|
+
@db = CouchRest.database!("#{couchdb_server}/sfw")
|
12
|
+
begin
|
13
|
+
@db.save_doc "_id" => "_design/recent-changes", :views => {}
|
14
|
+
rescue RestClient::Conflict
|
15
|
+
# design document already exists, do nothing
|
16
|
+
end
|
17
|
+
end
|
18
|
+
@db
|
19
|
+
end
|
20
|
+
|
21
|
+
### GET
|
22
|
+
|
23
|
+
def get_text(path)
|
24
|
+
path = relative_path(path)
|
25
|
+
begin
|
26
|
+
db.get(path)['data']
|
27
|
+
rescue RestClient::ResourceNotFound
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_blob(path)
|
33
|
+
blob = get_text path
|
34
|
+
Base64.decode64 blob if blob
|
35
|
+
end
|
36
|
+
|
37
|
+
### PUT
|
38
|
+
|
39
|
+
def put_text(path, text, metadata={})
|
40
|
+
path = relative_path(path)
|
41
|
+
metadata = metadata.each{ |k,v| metadata[k] = relative_path(v) }
|
42
|
+
attrs = {
|
43
|
+
'data' => text,
|
44
|
+
'updated_at' => Time.now.utc.iso8601
|
45
|
+
}.merge! metadata
|
46
|
+
|
47
|
+
begin
|
48
|
+
db.save_doc attrs.merge('_id' => path)
|
49
|
+
rescue RestClient::Conflict
|
50
|
+
doc = db.get path
|
51
|
+
doc.merge! attrs
|
52
|
+
doc.save
|
53
|
+
end
|
54
|
+
text
|
55
|
+
end
|
56
|
+
|
57
|
+
def put_blob(path, blob)
|
58
|
+
put_text path, Base64.strict_encode64(blob)
|
59
|
+
blob
|
60
|
+
end
|
61
|
+
|
62
|
+
### COLLECTIONS
|
63
|
+
|
64
|
+
def annotated_pages(pages_dir)
|
65
|
+
changes = pages pages_dir
|
66
|
+
changes.map do |change|
|
67
|
+
page = JSON.parse change['value']['data']
|
68
|
+
page.merge! 'updated_at' => Time.parse(change['value']['updated_at'])
|
69
|
+
page.merge! 'name' => change['value']['name']
|
70
|
+
page
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
### UTILITY
|
75
|
+
|
76
|
+
def pages(pages_dir)
|
77
|
+
pages_dir = relative_path pages_dir
|
78
|
+
pages_dir_safe = CGI.escape pages_dir
|
79
|
+
begin
|
80
|
+
db.view("recent-changes/#{pages_dir_safe}")['rows']
|
81
|
+
rescue RestClient::ResourceNotFound
|
82
|
+
create_view 'recent-changes', pages_dir
|
83
|
+
db.view("recent-changes/#{pages_dir_safe}")['rows']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_view(design_name, view_name)
|
88
|
+
design = db.get "_design/#{design_name}"
|
89
|
+
design['views'][view_name] = {
|
90
|
+
:map => "
|
91
|
+
function(doc) {
|
92
|
+
if (doc.directory == '#{view_name}')
|
93
|
+
emit(doc._id, doc)
|
94
|
+
}
|
95
|
+
"
|
96
|
+
}
|
97
|
+
design.save
|
98
|
+
end
|
99
|
+
|
100
|
+
def farm?(_)
|
101
|
+
!!ENV['FARM_MODE']
|
102
|
+
end
|
103
|
+
|
104
|
+
def mkdir(_)
|
105
|
+
# do nothing
|
106
|
+
end
|
107
|
+
|
108
|
+
def exists?(path)
|
109
|
+
!(get_text path).nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
def relative_path(path)
|
113
|
+
raise "Please set @app_root" unless @app_root
|
114
|
+
path.match(%r[^#{Regexp.escape @app_root}/?(.+?)$]) ? $1 : path
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class FileStore < Store
|
2
|
+
class << self
|
3
|
+
|
4
|
+
### GET
|
5
|
+
|
6
|
+
def get_text(path)
|
7
|
+
File.read path if File.exist? path
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_blob(path)
|
11
|
+
File.binread path if File.exist? path
|
12
|
+
end
|
13
|
+
|
14
|
+
### PUT
|
15
|
+
|
16
|
+
def put_text(path, text, metadata=nil)
|
17
|
+
# Note: metadata is ignored for filesystem storage
|
18
|
+
File.open(path, 'w'){ |file| file.write text }
|
19
|
+
text
|
20
|
+
end
|
21
|
+
|
22
|
+
def put_blob(path, blob)
|
23
|
+
File.open(path, 'wb'){ |file| file.write blob }
|
24
|
+
blob
|
25
|
+
end
|
26
|
+
|
27
|
+
### COLLECTIONS
|
28
|
+
|
29
|
+
def annotated_pages(pages_dir)
|
30
|
+
Dir.foreach(pages_dir).reject{|name|name =~ /^\./}.collect do |name|
|
31
|
+
page = get_page(File.join pages_dir, name)
|
32
|
+
page.merge!({
|
33
|
+
'name' => name,
|
34
|
+
'updated_at' => File.new("#{pages_dir}/#{name}").mtime
|
35
|
+
})
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
### UTILITY
|
40
|
+
|
41
|
+
def farm?(data_root)
|
42
|
+
ENV['FARM_MODE'] || File.exists?(File.join data_root, "farm")
|
43
|
+
end
|
44
|
+
|
45
|
+
def mkdir(directory)
|
46
|
+
FileUtils.mkdir_p directory
|
47
|
+
end
|
48
|
+
|
49
|
+
def exists?(path)
|
50
|
+
File.exists?(path)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Store
|
2
|
+
class << self
|
3
|
+
|
4
|
+
attr_writer :app_root
|
5
|
+
|
6
|
+
def set(store_classname, app_root)
|
7
|
+
# @store_class is literally the class FileStore by default, or if a class name is passed in, another subclass of Store
|
8
|
+
@store_class = store_classname ? Kernel.const_get(store_classname) : FileStore
|
9
|
+
@store_class.app_root = app_root
|
10
|
+
@store_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(*args)
|
14
|
+
# For any method not implemented in *this* class, pass the method call through to the designated Store subclass
|
15
|
+
@store_class.send(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
### GET
|
19
|
+
|
20
|
+
def get_hash(path)
|
21
|
+
json = get_text path
|
22
|
+
JSON.parse json if json
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :get_page, :get_hash
|
26
|
+
|
27
|
+
### PUT
|
28
|
+
|
29
|
+
def put_hash(path, ruby_data, metadata={})
|
30
|
+
json = JSON.pretty_generate(ruby_data)
|
31
|
+
put_text path, json, metadata
|
32
|
+
ruby_data
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :put_page, :put_hash
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/wiki/version.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module.exports = function (grunt) {
|
2
|
+
grunt.loadNpmTasks('grunt-browserify');
|
3
|
+
grunt.loadNpmTasks('grunt-contrib-coffee');
|
4
|
+
grunt.loadNpmTasks('grunt-contrib-watch');
|
5
|
+
|
6
|
+
grunt.initConfig({
|
7
|
+
browserify: {
|
8
|
+
client: {
|
9
|
+
src: ['client.coffee'],
|
10
|
+
dest: 'client.js',
|
11
|
+
options: {
|
12
|
+
transform: ['coffeeify'],
|
13
|
+
debug: true
|
14
|
+
}
|
15
|
+
},
|
16
|
+
testClient: {
|
17
|
+
src: ['testclient.coffee', 'plugins/*/test.coffee'],
|
18
|
+
dest: 'test/testclient.js',
|
19
|
+
options: {
|
20
|
+
transform: ['coffeeify'],
|
21
|
+
debug: true
|
22
|
+
}
|
23
|
+
}
|
24
|
+
},
|
25
|
+
|
26
|
+
coffee: {
|
27
|
+
plugins: {
|
28
|
+
expand: true,
|
29
|
+
src: ['plugins/**/*.coffee'],
|
30
|
+
ext: '.js'
|
31
|
+
}
|
32
|
+
},
|
33
|
+
|
34
|
+
watch: {
|
35
|
+
all: {
|
36
|
+
files: [
|
37
|
+
'<%= browserify.testClient.src %>',
|
38
|
+
'<%= browserify.client.src %>',
|
39
|
+
'<%= coffee.plugins.src %>',
|
40
|
+
'lib/**/*.coffee'
|
41
|
+
],
|
42
|
+
tasks: ['browserify', 'coffee']
|
43
|
+
}
|
44
|
+
}
|
45
|
+
});
|
46
|
+
|
47
|
+
grunt.registerTask('build', ['coffee', 'browserify']);
|
48
|
+
grunt.registerTask('default', ['build']);
|
49
|
+
|
50
|
+
};
|
@@ -0,0 +1,67 @@
|
|
1
|
+
Client Goals
|
2
|
+
============
|
3
|
+
|
4
|
+
A server offers direct restful read/write access to pages it owns and proxy access to pages held elsewhere in federated space.
|
5
|
+
A page is owned if it was created with the server or has been cloned and edited such that it is believed to be the most authoritative copy of a page previously owned elsewhere.
|
6
|
+
A server operates as a proxy to the rest of the federated wiki.
|
7
|
+
In this role it reformats data and metadata providing a unified experience.
|
8
|
+
It is welcome to collect behavioral statistics in order to improve this experience by anticipating permitted peer-to-peer server operations.
|
9
|
+
|
10
|
+
In summary, the server's client side exists to:
|
11
|
+
|
12
|
+
* Offer to a user a browsing experience that is independent of any specific server.
|
13
|
+
* Support writing, editing and curating of one server in a way that offers suitable influence over others.
|
14
|
+
|
15
|
+
Working with Browserify
|
16
|
+
=======================
|
17
|
+
|
18
|
+
The client side is written in CoffeeScript, and built with Browserify.
|
19
|
+
If you are not checking in changes you need not concern yourself with this.
|
20
|
+
We've checked in the generated Javascript for the client application.
|
21
|
+
|
22
|
+
If you do want to check in changes, install node v0.6.x
|
23
|
+
|
24
|
+
* On Linux download the source from [GitHub](https://github.com/joyent/node)
|
25
|
+
* On Windows get the installer from the [main node.js site](http://nodejs.org).
|
26
|
+
* On Mac you should be able to choose either.
|
27
|
+
|
28
|
+
Once node is installed come back to this directory and run:
|
29
|
+
|
30
|
+
* `npm install` To install CoffeeScript, Browserify, and all their dependencies.
|
31
|
+
|
32
|
+
You can now use:
|
33
|
+
|
34
|
+
* `npm start` To build the main client.
|
35
|
+
* `npm test` To build the test client.
|
36
|
+
|
37
|
+
These commands build client.js and test/testclient.js from client.coffee and
|
38
|
+
testclient.coffee respectively. They use their entry files to require the
|
39
|
+
rest of the coffee script they need from the source CS files in /lib.
|
40
|
+
|
41
|
+
We also have a cool automated talking (Mac only) Perl build script that uses
|
42
|
+
a globally installed browserify via `npm install -g browserify`, it watches
|
43
|
+
for changes, builds the clients automatically, and gives a verbal report
|
44
|
+
when you have syntax errors.
|
45
|
+
|
46
|
+
Testing
|
47
|
+
=======
|
48
|
+
|
49
|
+
All the client tests can be run by visiting /runtests.html on your server
|
50
|
+
or by running the full ruby test suite. Information about the libraries we
|
51
|
+
are using for testing can be found at:
|
52
|
+
|
53
|
+
* http://visionmedia.github.com/mocha/
|
54
|
+
* https://github.com/LearnBoost/expect.js
|
55
|
+
* http://sinonjs.org/
|
56
|
+
|
57
|
+
CoffeeScript hints
|
58
|
+
==================
|
59
|
+
|
60
|
+
We recommend taking time to learn the CoffeeScript syntax and the rationale for the Javascript idioms it employs. Start here:
|
61
|
+
|
62
|
+
http://jashkenas.github.com/coffee-script/
|
63
|
+
|
64
|
+
We used a Javascript to Coffeescript converter to create the first draft of client.coffee. You may find this converter useful for importing sample codes.
|
65
|
+
|
66
|
+
http://ricostacruz.com/js2coffee/
|
67
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
@ECHO OFF
|
2
|
+
::
|
3
|
+
:: Used on Windows to build testclient.js as npm start and test don't work!
|
4
|
+
::
|
5
|
+
|
6
|
+
:: Build testclient.js - need to expand .\plugins\*\test.coffee as wildcard does not work on Windows
|
7
|
+
|
8
|
+
echo "Building test\testclient.js"
|
9
|
+
|
10
|
+
.\node_modules\.bin\browserify.cmd testclient.coffee .\plugins\calendar\test.coffee .\plugins\changes\test.coffee .\plugins\efficiency\test.coffee .\plugins\report\test.coffee .\plugins\txtzyme\test.coffee -o test\testclient.js
|