wiki 0.0.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.
- 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
|