patricia 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +30 -0
- data/bin/patricia +2 -0
- data/lib/patricia/app.rb +67 -4
- data/lib/patricia/assets/javascripts/app.js +13 -9
- data/lib/patricia/assets/javascripts/editor.js +269 -0
- data/lib/patricia/assets/stylesheets/app.css +83 -0
- data/lib/patricia/cli.rb +12 -3
- data/lib/patricia/patricia.rb +5 -1
- data/lib/patricia/version.rb +1 -1
- data/lib/patricia/views/wiki/page.haml +41 -13
- data/lib/patricia/views/wiki/welcome.md +27 -1
- data/spec/app_spec.rb +33 -3
- data/spec/random-test-wiki/colors/cyan.html +18 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67fe48708657dc1541204315dcaedcb79f04479a
|
4
|
+
data.tar.gz: a3ebb2cc08e9f902773f89aa3c1d4894f9d1ec82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60803a1932ff8c839e000a8c1783af3de02e9a5a639e6b8df09c015098a4fa487c9f456974cbf2de51c6a60a14b9a421c0b99fad0790d1b6f979332c10218148
|
7
|
+
data.tar.gz: 6227b21360ab4648b651d0754da183b4c4b97da7a207ff0ef1a7dbd83f92a6a6cfbb2451b97780dbc0f3d9b696dca5b432c8d37221b9f32c48241b6ff61d2155
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -19,6 +19,16 @@ right file extension. Static files (images/PDFs/video/...) in the markup
|
|
19
19
|
directory are served as is, so you can link to them from any page and the
|
20
20
|
links will not break.
|
21
21
|
|
22
|
+
Patricia also comes with some optional goodies:
|
23
|
+
|
24
|
+
- Basic markup editor (`-e` flag) that allows you to select an element or
|
25
|
+
highlight text on a page and brings up the corresponding markup, if it
|
26
|
+
can figure it out.
|
27
|
+
- Tooltips (`-t` flag) which show you the full file path for every
|
28
|
+
directory and file in the sidebar by hovering over it.
|
29
|
+
- RegEx search across all pages (if you tag your pages using a consistent
|
30
|
+
pattern, you can use this to get a list of all pages for certain tags).
|
31
|
+
|
22
32
|
## Installation
|
23
33
|
|
24
34
|
gem install patricia
|
@@ -75,3 +85,23 @@ To add support for a new language do this:
|
|
75
85
|
possible, so that browsers will display them on a page instead of
|
76
86
|
prompting to download the file.
|
77
87
|
5. *Make sure the tests pass*
|
88
|
+
|
89
|
+
## Editor
|
90
|
+
|
91
|
+
Patricia's rudimentary and completely optional markup editor can
|
92
|
+
be enabled using the `-e` flat. It allows you to edit the markup for
|
93
|
+
editable components by hovering over them or by simply selecting text. It
|
94
|
+
tries its best to figure out which part of the markup is responsible for
|
95
|
+
the selected element and highlights it so that one can immediately jump to
|
96
|
+
the corresponding markup for quick fixes or even larger changes. Keep in
|
97
|
+
mind that this is not perfect, but it definetly is usable and is quite
|
98
|
+
handy for a lot of small fixes across multiple files as well as correcting
|
99
|
+
typos.
|
100
|
+
|
101
|
+
## Page search
|
102
|
+
|
103
|
+
There is the search box in the sidebar that allows you to quickly search
|
104
|
+
all page titles, but there is also a buillt-in search page that allows you
|
105
|
+
to search the actual text of each page using regular expressions. So if you
|
106
|
+
include (hash) tags on your pages, you can use this to search all matching
|
107
|
+
pages, for instance.
|
data/bin/patricia
CHANGED
@@ -11,6 +11,8 @@ config = {
|
|
11
11
|
:js_dir => CLI.options[:js],
|
12
12
|
:markup_dir => CLI.options[:markup_dir],
|
13
13
|
:tooltips => CLI.options[:tooltips],
|
14
|
+
:editor => CLI.options[:editor],
|
15
|
+
:gfm => CLI.options[:gfm],
|
14
16
|
}
|
15
17
|
config_file = File.dirname(__FILE__) + '/app_config.yml'
|
16
18
|
# The thread ensures that the config file is written before the Sinatra app
|
data/lib/patricia/app.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'sinatra/base'
|
2
2
|
require 'yaml'
|
3
|
+
require 'json'
|
4
|
+
require 'timeout'
|
3
5
|
|
4
6
|
|
5
7
|
module PatriciaApp
|
@@ -36,6 +38,8 @@ module PatriciaApp
|
|
36
38
|
set :app_js_path, '/patricia.js'
|
37
39
|
set :app_public_folder, app_configs[:public_folder]
|
38
40
|
set :app_tooltips, app_configs[:tooltips]
|
41
|
+
set :app_editor, app_configs[:editor]
|
42
|
+
set :app_gfm, app_configs[:gfm]
|
39
43
|
# CSS
|
40
44
|
if app_configs[:css_dir] != nil
|
41
45
|
set :app_css_dir, app_configs[:css_dir]
|
@@ -79,10 +83,12 @@ module PatriciaApp
|
|
79
83
|
'sh' => 'application/x-sh',
|
80
84
|
'7z' => 'application/x-7z-compressed',
|
81
85
|
'swf' => 'application/x-shockwave-flash',
|
86
|
+
'xml' => 'application/xml;charset=utf-8',
|
82
87
|
'avi' => 'video/x-msvideo',
|
83
88
|
'txt' => 'text/plain',
|
84
89
|
'css' => 'text/css',
|
85
90
|
'csv' => 'text/csv',
|
91
|
+
'html' => 'text/html',
|
86
92
|
# Send markup source files
|
87
93
|
#
|
88
94
|
# One can argue if this is semantically correct (alternative:
|
@@ -121,8 +127,14 @@ module PatriciaApp
|
|
121
127
|
end
|
122
128
|
|
123
129
|
get '/' do
|
124
|
-
|
130
|
+
if settings.app_gfm
|
131
|
+
html = Patricia::Converter.gfm_to_html(File.dirname(__FILE__) +
|
132
|
+
'/views/wiki/welcome.md')
|
133
|
+
else
|
134
|
+
html = Patricia::Converter.to_html(File.dirname(__FILE__) +
|
125
135
|
'/views/wiki/welcome.md')
|
136
|
+
end
|
137
|
+
@html = '<div id="p-welcome-text-page" class="">' + html + '</div>'
|
126
138
|
@toc = build_toc
|
127
139
|
@title = generate_page_title
|
128
140
|
@page_title = ''
|
@@ -161,11 +173,11 @@ module PatriciaApp
|
|
161
173
|
beautiful_file_name = capitalize_all(file_name.gsub(/-/, ' '))
|
162
174
|
beautiful_path = no_ext.split('/').collect do |s|
|
163
175
|
capitalize_all(s.gsub(/-/, ' '))
|
164
|
-
end.join(' > ')
|
176
|
+
end.delete_if(&:empty?).join(' > ')
|
165
177
|
content = File.read(path)
|
166
178
|
lines = content.split("\n").length
|
167
179
|
if content =~ search_query
|
168
|
-
@results << [beautiful_file_name,
|
180
|
+
@results << [beautiful_file_name, no_ext, beautiful_path,
|
169
181
|
lines]
|
170
182
|
end
|
171
183
|
end
|
@@ -173,6 +185,52 @@ module PatriciaApp
|
|
173
185
|
haml :search, :layout => :application
|
174
186
|
end
|
175
187
|
|
188
|
+
post %r{/patricia/offsets/?} do
|
189
|
+
if settings.app_editor
|
190
|
+
file_content = File.read(File.join(settings.app_markup_dir,
|
191
|
+
params[:markup_url]))
|
192
|
+
offsets = []
|
193
|
+
# Time out if the RegEx is too complex.
|
194
|
+
begin
|
195
|
+
Timeout::timeout(3) do
|
196
|
+
filler = " *\n*\w*"
|
197
|
+
pattern = Regexp.new(filler, Regexp::MULTILINE)
|
198
|
+
params[:string].split.each do |token|
|
199
|
+
pattern =
|
200
|
+
Regexp.new(pattern.to_s + filler + Regexp.quote(token),
|
201
|
+
Regexp::MULTILINE)
|
202
|
+
end
|
203
|
+
pattern = Regexp.new(pattern.to_s + filler, Regexp::MULTILINE)
|
204
|
+
|
205
|
+
# Skip empty/whitespace only queries.
|
206
|
+
if !pattern.to_s.empty? && !(pattern.to_s =~ /^\s*$/)
|
207
|
+
file_content.scan(pattern) do |c|
|
208
|
+
offsets << [$~.offset(0)[0], $~.offset(0)[1]]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
rescue
|
213
|
+
# Ignore.
|
214
|
+
end
|
215
|
+
|
216
|
+
content_type 'json'
|
217
|
+
{:markup => file_content, :offsets => offsets}.to_json
|
218
|
+
else
|
219
|
+
redirect to('/404')
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
post %r{/patricia/edit/?} do
|
224
|
+
if settings.app_editor
|
225
|
+
File.open(File.join(settings.app_markup_dir,
|
226
|
+
params[:markup_url]), 'w') do |f|
|
227
|
+
f.puts(params[:string])
|
228
|
+
end
|
229
|
+
else
|
230
|
+
redirect to('/404')
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
176
234
|
get settings.app_css_path do
|
177
235
|
pwd = File.dirname(__FILE__)
|
178
236
|
css = ''
|
@@ -196,6 +254,7 @@ module PatriciaApp
|
|
196
254
|
'/assets/javascripts/app.js',
|
197
255
|
]
|
198
256
|
files << '/assets/javascripts/tooltips.js' if settings.app_tooltips
|
257
|
+
files << '/assets/javascripts/editor.js' if settings.app_editor
|
199
258
|
files.each do |path|
|
200
259
|
js << File.read(pwd + path) + "\n\n"
|
201
260
|
end
|
@@ -215,7 +274,11 @@ module PatriciaApp
|
|
215
274
|
file_path = Dir[settings.app_markup_dir + '/' + path +
|
216
275
|
settings.app_markdown_glob].first
|
217
276
|
@markup_url = file_path.gsub(/#{settings.app_markup_dir}/, '')
|
218
|
-
|
277
|
+
if settings.app_gfm
|
278
|
+
@html = Patricia::Converter.gfm_to_html(file_path)
|
279
|
+
else
|
280
|
+
@html = Patricia::Converter.to_html(file_path)
|
281
|
+
end
|
219
282
|
@toc = build_toc
|
220
283
|
arrow = ' > '
|
221
284
|
@title = generate_page_title
|
@@ -1,21 +1,20 @@
|
|
1
1
|
$(document).ready(function() {
|
2
2
|
|
3
|
+
var self = $(this);
|
4
|
+
|
3
5
|
// Helpers
|
4
6
|
|
5
7
|
$.fn.elementText = function() {
|
6
|
-
|
7
|
-
$(this).contents().each(function() {
|
8
|
-
if ($(this).nodeType == 3) {
|
9
|
-
str += $(this).textContent || $(this).innerText || '';
|
10
|
-
}
|
11
|
-
});
|
12
|
-
return str;
|
8
|
+
return $(this).clone().children().remove().end().text();
|
13
9
|
};
|
14
10
|
|
11
|
+
// Aesthetical enhancements
|
12
|
+
|
13
|
+
$('table').addClass('table table-striped table-responsive')
|
14
|
+
|
15
15
|
|
16
16
|
// Sidebar expanding
|
17
17
|
|
18
|
-
var self = $(this);
|
19
18
|
self.sidebarExpanded = false;
|
20
19
|
self.originalSidebarStyle = {
|
21
20
|
'width': '',
|
@@ -82,6 +81,11 @@ href="/">Close</a></p>\
|
|
82
81
|
<p><code>s</code><span>: Select sidebar search box</span></p>\
|
83
82
|
<p><code>Esc</code><span>: Unselect sidebar search box</span></p>\
|
84
83
|
<p><code>p</code><span>: Go to page search page</span></p>\
|
84
|
+
<p><code>e</code><span>: Edit selected text</span></p>\
|
85
|
+
<strong>Editor textarea:</strong>\
|
86
|
+
<p><code>Ctrl</code> + <code>RET</code><span>: Save changes \
|
87
|
+
<p><code>Alt</code> + <code>p</code><span>: Go to previous match \
|
88
|
+
<p><code>Alt</code> + <code>n</code><span>: Go to next match \
|
85
89
|
</div>\
|
86
90
|
'
|
87
91
|
);
|
@@ -94,7 +98,7 @@ href="/">Close</a></p>\
|
|
94
98
|
'top': '50%',
|
95
99
|
'left': '50%',
|
96
100
|
'width': '500px',
|
97
|
-
'height': '
|
101
|
+
'height': '430px',
|
98
102
|
'margin-left': '-200px',
|
99
103
|
'margin-top': '-300px',
|
100
104
|
'z-index': '9999',
|
@@ -0,0 +1,269 @@
|
|
1
|
+
$(document).ready(function() {
|
2
|
+
|
3
|
+
var self = $(this);
|
4
|
+
|
5
|
+
// Helpers
|
6
|
+
|
7
|
+
self.getSelectedText = function() {
|
8
|
+
var text = '';
|
9
|
+
if (window.getSelection) {
|
10
|
+
text = window.getSelection().toString();
|
11
|
+
} else if (document.getSelection) {
|
12
|
+
text = document.getSelection().toString();
|
13
|
+
} else if (document.selection) {
|
14
|
+
text = document.selection.createRange().text;
|
15
|
+
}
|
16
|
+
return text;
|
17
|
+
};
|
18
|
+
|
19
|
+
self.scrollToSelectedText = function(textArea, start, end) {
|
20
|
+
var charsPerRow = textArea.attr('cols');
|
21
|
+
var selectionRow = (start - (start % charsPerRow)) / charsPerRow;
|
22
|
+
var lineHeight = textArea.height() / textArea.attr('rows');
|
23
|
+
textArea.scrollTop(lineHeight * selectionRow +
|
24
|
+
(textArea.height() / 2));
|
25
|
+
};
|
26
|
+
|
27
|
+
|
28
|
+
// Editing
|
29
|
+
|
30
|
+
self.hoveredText = '';
|
31
|
+
self.hoveredElementBgColor = '';
|
32
|
+
self.hoveredElementHighlightColor = '#DFDFD2';
|
33
|
+
self.editButton = $('#edit-button');
|
34
|
+
self.mouseX = 0;
|
35
|
+
self.mouseY = 0;
|
36
|
+
$(document).mousemove(function(e) {
|
37
|
+
self.mouseX = e.pageX;
|
38
|
+
self.mouseY = e.pageY;
|
39
|
+
});
|
40
|
+
|
41
|
+
self.currentlyHighlightedText = '';
|
42
|
+
|
43
|
+
// Get all leaf node children of `#content'.
|
44
|
+
//
|
45
|
+
// Alternative: The following is a method to get only the first level
|
46
|
+
// children of `#content'.
|
47
|
+
//
|
48
|
+
// $('#content > *:not(:has(*))').hover(function() {
|
49
|
+
//
|
50
|
+
$('#content *').filter(function(index) {
|
51
|
+
var isLeaf = $(this).children().length == 0;
|
52
|
+
return isLeaf;
|
53
|
+
}).hover(function() {
|
54
|
+
if (!$('#p-welcome-text-page').length) {
|
55
|
+
$(this).css({'background-color': self.hoveredElementHighlightColor});
|
56
|
+
}
|
57
|
+
// Postion the edit button
|
58
|
+
if (self.editButton.hasClass('hidden')) {
|
59
|
+
self.editButton.removeClass('hidden');
|
60
|
+
} else {
|
61
|
+
self.editButton.toggle();
|
62
|
+
}
|
63
|
+
var that = $(this);
|
64
|
+
that.setCurrentlyHighlightedText = function() {
|
65
|
+
self.currentlyHighlightedText = that.elementText();
|
66
|
+
};
|
67
|
+
// Append the edit button after setting
|
68
|
+
// `self.currentlyHighlightedText' or else the text will include the
|
69
|
+
// edit button text.
|
70
|
+
$.when(that.setCurrentlyHighlightedText()).done(function() {
|
71
|
+
that.append(self.editButton);
|
72
|
+
});
|
73
|
+
}, function() {
|
74
|
+
$(this).css({'background-color': self.hoveredElementBgColor});
|
75
|
+
// Hide the edit button.
|
76
|
+
self.editButton.toggle();
|
77
|
+
// Remove it from the previous element, but keep it in the DOM. If it
|
78
|
+
// is not in the DOM, it is not clickable.
|
79
|
+
$('body').append(self.editButton);
|
80
|
+
});
|
81
|
+
|
82
|
+
self.offsets = [];
|
83
|
+
self.currentOffset = [];
|
84
|
+
self.currentOffsetIndex = 0;
|
85
|
+
self.textArea = $('#edit-modal textarea');
|
86
|
+
self.editModal = $('#edit-modal');
|
87
|
+
self.editNavigationBox = $('#edit-navigation-box');
|
88
|
+
self.editSelectedText = function() {
|
89
|
+
$.ajax({
|
90
|
+
'type': 'POST',
|
91
|
+
'url': self.editButton.attr('href'),
|
92
|
+
'data': {
|
93
|
+
'string': self.currentlyHighlightedText,
|
94
|
+
'markup_url': self.editButton.attr('data-markup-url'),
|
95
|
+
},
|
96
|
+
'success': function(data, status) {
|
97
|
+
// Show the edit modal
|
98
|
+
self.editModal.modal('show');
|
99
|
+
$('#currently-selected-text-info').text(
|
100
|
+
self.currentlyHighlightedText);
|
101
|
+
self.textArea.val(data['markup']);
|
102
|
+
self.textArea.focus();
|
103
|
+
self.offsets = 0; // Reset.
|
104
|
+
self.offsets = data['offsets']
|
105
|
+
self.currentOffsetIndex = 0;
|
106
|
+
self.currentOffset = self.offsets[self.currentOffsetIndex] ||
|
107
|
+
[0, 0];
|
108
|
+
$('#current-offset-index').text(self.currentOffsetIndex + 1 +
|
109
|
+
' / ');
|
110
|
+
$('#offsets-length').text(self.offsets.length);
|
111
|
+
if (self.offsets.length > 1) {
|
112
|
+
$('#edit-navigation-box').show();
|
113
|
+
self.editNavigationBox.show();
|
114
|
+
} else {
|
115
|
+
self.editNavigationBox.hide();
|
116
|
+
}
|
117
|
+
|
118
|
+
self.editModal.on('shown.bs.modal', function() {
|
119
|
+
self.textArea.focus();
|
120
|
+
self.textArea[0].setSelectionRange(self.currentOffset[0],
|
121
|
+
self.currentOffset[1])
|
122
|
+
self.scrollToSelectedText(self.textArea, self.currentOffset[0],
|
123
|
+
self.currentOffset[1]);
|
124
|
+
});
|
125
|
+
},
|
126
|
+
'data-type': 'json',
|
127
|
+
});
|
128
|
+
};
|
129
|
+
|
130
|
+
self.editButton.on('click', function(e) {
|
131
|
+
e.preventDefault();
|
132
|
+
self.leftEditButton.hide();
|
133
|
+
self.editSelectedText();
|
134
|
+
});
|
135
|
+
|
136
|
+
self.leftEditButton = $('#left-edit-button');
|
137
|
+
|
138
|
+
self.leftEditButton.on('click', function(e) {
|
139
|
+
e.preventDefault();
|
140
|
+
// e.stopPropagation();
|
141
|
+
self.leftEditButton.hide();
|
142
|
+
self.editSelectedText();
|
143
|
+
});
|
144
|
+
|
145
|
+
$('#content').on('mouseup', function(e) {
|
146
|
+
if (self.getSelectedText() != '') {
|
147
|
+
self.currentlyHighlightedText = self.getSelectedText();
|
148
|
+
}
|
149
|
+
if (self.getSelectedText() != '') {
|
150
|
+
// if (self.currentlyHighlightedText != '') {
|
151
|
+
if (self.leftEditButton.hasClass('hidden')) {
|
152
|
+
self.leftEditButton.removeClass('hidden');
|
153
|
+
self.leftEditButton.show();
|
154
|
+
} else {
|
155
|
+
self.leftEditButton.show();
|
156
|
+
}
|
157
|
+
self.leftEditButton.css({
|
158
|
+
'position': 'absolute',
|
159
|
+
'top': self.mouseY - (self.leftEditButton.height() + 20),
|
160
|
+
'left': self.mouseX - (self.leftEditButton.width()),
|
161
|
+
'z-index': 9998,
|
162
|
+
'box-shadow': '0px 5px 3px rgba(0, 0, 0, 0.3), \
|
163
|
+
0px 0px 8px rgba(102, 175, 233, 0.6)',
|
164
|
+
'background-color': '#121212',
|
165
|
+
'color': '#F1F1F1',
|
166
|
+
'margin-top': '0',
|
167
|
+
'font-weight': 'bold',
|
168
|
+
'border': 'none',
|
169
|
+
'border-radius': '5px',
|
170
|
+
'border': '2px solid #F1F1F1',
|
171
|
+
});
|
172
|
+
self.leftEditButton.animate({
|
173
|
+
'margin-top': '-10px',
|
174
|
+
}, 230);
|
175
|
+
}
|
176
|
+
});
|
177
|
+
|
178
|
+
$('*').not(self.leftEditButton).on('mousedown', function(e) {
|
179
|
+
e.stopPropagation();
|
180
|
+
window.setTimeout(function() {
|
181
|
+
self.leftEditButton.hide();
|
182
|
+
}, 130);
|
183
|
+
});
|
184
|
+
|
185
|
+
$(document).keypress(function() {
|
186
|
+
self.leftEditButton.hide();
|
187
|
+
});
|
188
|
+
|
189
|
+
// Navigation between matches
|
190
|
+
|
191
|
+
$('#previous-match-button').click(function(e) {
|
192
|
+
e.preventDefault();
|
193
|
+
if (self.currentOffsetIndex > 0) {
|
194
|
+
self.currentOffsetIndex -= 1;
|
195
|
+
} else {
|
196
|
+
self.currentOffsetIndex = self.offsets.length - 1;
|
197
|
+
}
|
198
|
+
self.currentOffset = self.offsets[self.currentOffsetIndex];
|
199
|
+
self.textArea.focus();
|
200
|
+
self.textArea[0].setSelectionRange(self.currentOffset[0],
|
201
|
+
self.currentOffset[1])
|
202
|
+
self.scrollToSelectedText(self.textArea, self.currentOffset[0],
|
203
|
+
self.currentOffset[1]);
|
204
|
+
$('#current-offset-index').text(self.currentOffsetIndex + 1 + ' / ');
|
205
|
+
});
|
206
|
+
|
207
|
+
$('#next-match-button').click(function(e) {
|
208
|
+
e.preventDefault();
|
209
|
+
if (self.currentOffsetIndex < self.offsets.length - 1) {
|
210
|
+
self.currentOffsetIndex += 1;
|
211
|
+
} else {
|
212
|
+
self.currentOffsetIndex = 0;
|
213
|
+
}
|
214
|
+
self.currentOffset = self.offsets[self.currentOffsetIndex];
|
215
|
+
self.textArea.focus();
|
216
|
+
self.textArea[0].setSelectionRange(self.currentOffset[0],
|
217
|
+
self.currentOffset[1])
|
218
|
+
self.scrollToSelectedText(self.textArea, self.currentOffset[0],
|
219
|
+
self.currentOffset[1]);
|
220
|
+
$('#current-offset-index').text(self.currentOffsetIndex + 1 + ' / ');
|
221
|
+
});
|
222
|
+
|
223
|
+
|
224
|
+
// 101: e - Edit selected text
|
225
|
+
$(document).on('keypress', function(e) {
|
226
|
+
if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA') {
|
227
|
+
if (e.which == 101) {
|
228
|
+
self.editSelectedText();
|
229
|
+
}
|
230
|
+
}
|
231
|
+
});
|
232
|
+
|
233
|
+
// Saving
|
234
|
+
|
235
|
+
self.saveButton = $('#edit-save-button');
|
236
|
+
self.saveButton.on('click', function() {
|
237
|
+
$.ajax({
|
238
|
+
'type': 'POST',
|
239
|
+
'url': self.saveButton.attr('data-url'),
|
240
|
+
'data': {
|
241
|
+
'markup_url': self.editButton.attr('data-markup-url'),
|
242
|
+
'string': self.textArea.val(),
|
243
|
+
},
|
244
|
+
});
|
245
|
+
location.reload();
|
246
|
+
});
|
247
|
+
|
248
|
+
// 17: Ctrl
|
249
|
+
// 13: RET
|
250
|
+
// 18: Alt
|
251
|
+
// 80: p
|
252
|
+
// 78: n
|
253
|
+
self.keys = {};
|
254
|
+
self.textArea.on('keydown', function(e) {
|
255
|
+
self.keys[e.which] = true;
|
256
|
+
if (self.keys[13] == true && self.keys[17] == true) {
|
257
|
+
self.saveButton.click();
|
258
|
+
} else if (self.keys[18] == true && self.keys[80] == true) {
|
259
|
+
$('#previous-match-button').click();
|
260
|
+
} else if (self.keys[18] == true && self.keys[78] == true) {
|
261
|
+
$('#next-match-button').click();
|
262
|
+
}
|
263
|
+
});
|
264
|
+
|
265
|
+
self.textArea.on('keyup', function(e) {
|
266
|
+
delete self.keys[e.which];
|
267
|
+
});
|
268
|
+
|
269
|
+
});
|
@@ -20,3 +20,86 @@ img {
|
|
20
20
|
max-width: 100%;
|
21
21
|
height: auto;
|
22
22
|
}
|
23
|
+
|
24
|
+
#edit-button {
|
25
|
+
margin-left: 10px;
|
26
|
+
z-index: 9998;
|
27
|
+
}
|
28
|
+
|
29
|
+
#edit-modal .modal-dialog {
|
30
|
+
width: 80%;
|
31
|
+
}
|
32
|
+
|
33
|
+
#edit-modal textarea {
|
34
|
+
font-family: monospace;
|
35
|
+
}
|
36
|
+
|
37
|
+
/* Page title */
|
38
|
+
|
39
|
+
.page-title {
|
40
|
+
line-height: 30px;
|
41
|
+
border-top: 1px solid #E3E3E3;
|
42
|
+
}
|
43
|
+
|
44
|
+
body {
|
45
|
+
background-color: #F5F5F5;
|
46
|
+
margin-top: 20px;
|
47
|
+
}
|
48
|
+
|
49
|
+
#content {
|
50
|
+
background-color: #FFFFFF;
|
51
|
+
color: #393939;
|
52
|
+
padding: 25px;
|
53
|
+
border: 1px solid #E3E3E3;
|
54
|
+
border-radius: 4px;
|
55
|
+
}
|
56
|
+
|
57
|
+
#p-sidebar {
|
58
|
+
background-color: #FFFFFF;
|
59
|
+
color: #393939;
|
60
|
+
border: 1px solid #E3E3E3;
|
61
|
+
border-radius: 4px;
|
62
|
+
}
|
63
|
+
|
64
|
+
.top-panel {
|
65
|
+
margin-bottom: 40px;
|
66
|
+
}
|
67
|
+
|
68
|
+
/* Welcome page */
|
69
|
+
|
70
|
+
#p-welcome-text-page h1 {
|
71
|
+
background-color: #428BCA;
|
72
|
+
padding: 10px;
|
73
|
+
padding-top: 15px;
|
74
|
+
color: #F1F1F1;
|
75
|
+
border-radius: 5px;
|
76
|
+
text-align: center;
|
77
|
+
}
|
78
|
+
|
79
|
+
#p-welcome-text-page {
|
80
|
+
border: 5px solid #E3E3E3;
|
81
|
+
background-color: #ECF4FF;
|
82
|
+
padding: 20px;
|
83
|
+
padding-top: 5px;
|
84
|
+
border-radius: 5px;
|
85
|
+
}
|
86
|
+
|
87
|
+
#p-welcome-text-page h2 {
|
88
|
+
background-color: #FFFFFF;
|
89
|
+
border: 5px solid #428BCA;
|
90
|
+
color: #428BCA;
|
91
|
+
padding: 10px;
|
92
|
+
padding-top: 15px;
|
93
|
+
border-radius: 5px;
|
94
|
+
text-align: center;
|
95
|
+
}
|
96
|
+
|
97
|
+
#currently-selected-text-info {
|
98
|
+
height: 50px;
|
99
|
+
overflow: hidden;
|
100
|
+
border: none !important;
|
101
|
+
}
|
102
|
+
|
103
|
+
#currently-selected-text-info:hover {
|
104
|
+
overflow: scroll;
|
105
|
+
}
|
data/lib/patricia/cli.rb
CHANGED
@@ -16,6 +16,11 @@ module CLI
|
|
16
16
|
opts.separator ''
|
17
17
|
opts.separator 'Options:'
|
18
18
|
|
19
|
+
opts.on('-p', '--port PORT', Integer,
|
20
|
+
'Port to run the server on') do |v|
|
21
|
+
options[:port] = v
|
22
|
+
end
|
23
|
+
|
19
24
|
opts.on('-c', '--css DIR', 'Directory with CSS files to be',
|
20
25
|
'included in the output',
|
21
26
|
' (All .css files in this directory',
|
@@ -35,9 +40,13 @@ module CLI
|
|
35
40
|
options[:tooltips] = v
|
36
41
|
end
|
37
42
|
|
38
|
-
opts.on('-
|
39
|
-
|
40
|
-
|
43
|
+
opts.on('-e', '--editor', 'Enable markup editor') do |v|
|
44
|
+
options[:editor] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('-g', '--gfm',
|
48
|
+
'Use GitHub Flavored Markdown for all', ' rendering') do |v|
|
49
|
+
options[:gfm] = v
|
41
50
|
end
|
42
51
|
|
43
52
|
# Description
|
data/lib/patricia/patricia.rb
CHANGED
@@ -3,6 +3,7 @@ require 'kramdown'
|
|
3
3
|
require 'org-ruby'
|
4
4
|
require 'redcloth'
|
5
5
|
require 'pandoc-ruby'
|
6
|
+
require 'github/markup'
|
6
7
|
|
7
8
|
|
8
9
|
module Patricia
|
@@ -51,10 +52,13 @@ module Patricia
|
|
51
52
|
end
|
52
53
|
|
53
54
|
def self.to_html(path)
|
54
|
-
markup = File.
|
55
|
+
markup = File.read(path)
|
55
56
|
self.send @@converters[File.extname(path).sub(/^\./, '')], markup
|
56
57
|
end
|
57
58
|
|
59
|
+
def self.gfm_to_html(path)
|
60
|
+
GitHub::Markup.render(path)
|
61
|
+
end
|
58
62
|
end
|
59
63
|
|
60
64
|
class Wiki
|
data/lib/patricia/version.rb
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
%br
|
2
2
|
%div.container
|
3
3
|
%div.row
|
4
|
-
%div.col.col-md-7.col-md-offset-1
|
5
|
-
%div.row
|
6
|
-
%div.col.col-md-
|
7
|
-
%
|
8
|
-
%strong
|
9
|
-
|
10
|
-
|
11
|
-
|
|
12
|
-
%span.text-muted= @breadcrumb
|
13
|
-
= @page_title if defined?(@page_title)
|
14
|
-
%div.col.col-md-4
|
4
|
+
%div.col.col-md-7.col-md-offset-1
|
5
|
+
%div.row.panel.panel-default.panel-body.top-panel
|
6
|
+
%div.col.col-md-9
|
7
|
+
%p
|
8
|
+
%strong
|
9
|
+
%a{:href => "/"}= @title if defined?(@title)
|
10
|
+
%div.col.col-md-3
|
15
11
|
%p.text-right
|
16
12
|
%a.text-muted.small#p-page-search-link{:href => "/patricia/search"} Search pages >
|
17
|
-
|
18
|
-
|
13
|
+
%div.row
|
14
|
+
%div.col.col-md-12.page-title
|
15
|
+
%span.text-muted= @breadcrumb
|
16
|
+
= @page_title if defined?(@page_title)
|
17
|
+
|
18
|
+
%div.row#content
|
19
|
+
= preserve do
|
20
|
+
= @html if defined?(@html)
|
19
21
|
%hr
|
20
22
|
- if defined?(@markup_url)
|
21
23
|
%p.text-center
|
@@ -29,3 +31,29 @@
|
|
29
31
|
- if defined?(@toc)
|
30
32
|
%div#toc
|
31
33
|
= @toc
|
34
|
+
- if defined?(@markup_url)
|
35
|
+
%a.hidden#edit-button{:href => "/patricia/offsets", 'data-markup-url' => "#{@markup_url}"} Edit
|
36
|
+
%a.hidden#left-edit-button.btn.btn-default{:href => "/patricia/offsets", 'data-markup-url' => "#{@markup_url}"} Edit
|
37
|
+
|
38
|
+
%div.modal.fade#edit-modal{:tabindex => '-1', :role => 'dialog'}
|
39
|
+
%div.modal-dialog.modal-lg
|
40
|
+
%div.modal-content
|
41
|
+
%div.modal-header
|
42
|
+
%button.close{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'} ×
|
43
|
+
%h4.no-style.modal-title Edit Page Markup
|
44
|
+
%div.modal-body
|
45
|
+
%pre#currently-selected-text-info{:title => 'Selected text', 'data-toggle' => 'tooltip', 'data-placement' => 'top'}
|
46
|
+
%textarea.form-control{:rows => 15, :cols => 100}
|
47
|
+
%div.container#edit-navigation-box
|
48
|
+
%div.row
|
49
|
+
%br
|
50
|
+
%div.col-md-2.col-md-offset-3
|
51
|
+
%a#previous-match-button.btn.btn-default.btn-block{:href => '#'} < Previous match
|
52
|
+
%div.col-md-1
|
53
|
+
%span#current-offset-index
|
54
|
+
%span#offsets-length
|
55
|
+
%div.col-md-2
|
56
|
+
%a#next-match-button.btn.btn-default.btn-block{:href => '#'} Next match >
|
57
|
+
%div.modal-footer
|
58
|
+
%button.btn.btn-default{:type => 'button', 'data-dismiss' => 'modal'} Cancel
|
59
|
+
%button.btn.btn-primary#edit-save-button{:type => 'button', 'data-dismiss' => 'modal', 'data-url' => '/patricia/edit'} Save
|
@@ -93,8 +93,34 @@ user, run:
|
|
93
93
|
## Sidebar
|
94
94
|
|
95
95
|
- The search box is case-insensitive, but otherwise RegEx-aware.
|
96
|
-
|
97
96
|
- The sidebar can be widened. Click `Widen sidebar` or hit the `w` key.
|
97
|
+
- Hit `s` to focus the search bar from wherever you are on a page.
|
98
|
+
|
99
|
+
## Editor
|
100
|
+
|
101
|
+
- Use the `-e` flag to enable the editor.
|
102
|
+
- There are three ways to edit text:
|
103
|
+
1. Hover over elements. Editable elements will present you with an `Edit`
|
104
|
+
link.
|
105
|
+
2. Hover over an editable element and press the `e` key.
|
106
|
+
3. Highlight some arbitrary text and press the `Edit` button that appears
|
107
|
+
or hit the `e` key.
|
108
|
+
- Patricia tries to figure out which markup source matches the selected
|
109
|
+
text. If there are multiple matches, you can step through all of them.
|
110
|
+
|
111
|
+
## Tooltips
|
112
|
+
|
113
|
+
... can be enabled using the `-t` flag and will show tooltips for every
|
114
|
+
sidebar item.
|
115
|
+
|
116
|
+
## Page Search
|
117
|
+
|
118
|
+
- There is a `Search pages` link on every page.
|
119
|
+
- The search is performed across all markup source files and will return a
|
120
|
+
list of matching pages.
|
121
|
+
- Regular expressions can be used.
|
122
|
+
|
123
|
+
## Keyboard Shortcuts
|
98
124
|
|
99
125
|
- Press `?` to get a list of all keyboard shortcuts.
|
100
126
|
|
data/spec/app_spec.rb
CHANGED
@@ -25,6 +25,7 @@ describe "PatriciaApp::App" do
|
|
25
25
|
:css_dir => nil,
|
26
26
|
:js_dir => nil,
|
27
27
|
:tooltips => false,
|
28
|
+
:editor => true,
|
28
29
|
}
|
29
30
|
load_config(config)
|
30
31
|
end
|
@@ -69,8 +70,8 @@ request" do
|
|
69
70
|
end
|
70
71
|
|
71
72
|
describe "Markup language: reStructuredText" do
|
72
|
-
it "reads a reStructuredText file and renders it as HTML upon a
|
73
|
-
request" do
|
73
|
+
it "reads a reStructuredText file and renders it as HTML upon a \
|
74
|
+
GET request" do
|
74
75
|
get '/colors/light-pink'
|
75
76
|
expect(last_response).to be_ok
|
76
77
|
expect(last_response.body)
|
@@ -83,8 +84,18 @@ request" do
|
|
83
84
|
describe "Return static files" do
|
84
85
|
describe "Static file: PNG image" do
|
85
86
|
it "returns a PNG image uppon a GET request" do
|
86
|
-
get '/colors/
|
87
|
+
get '/colors/image.png'
|
88
|
+
expect(last_response).to be_ok
|
89
|
+
expect(last_response.headers['Content-Type']).to eq('image/png')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "Static file: HTML image" do
|
94
|
+
it "returns a HTML image uppon a GET request" do
|
95
|
+
get '/colors/cyan.html'
|
87
96
|
expect(last_response).to be_ok
|
97
|
+
expect(last_response.headers['Content-Type'])
|
98
|
+
.to eq('text/html;charset=utf-8')
|
88
99
|
end
|
89
100
|
end
|
90
101
|
|
@@ -142,6 +153,24 @@ upon a GET request" do
|
|
142
153
|
end
|
143
154
|
end
|
144
155
|
|
156
|
+
# Page Editing
|
157
|
+
|
158
|
+
describe "Page Editing" do
|
159
|
+
it "finds all matches for a sting in a page's markup and returns \
|
160
|
+
the markup together with the match offsets (POST)" do
|
161
|
+
post '/patricia/offsets', {:markup_url =>
|
162
|
+
'colors/red.md', :string => 'red'}
|
163
|
+
expect(last_response).to be_ok
|
164
|
+
end
|
165
|
+
|
166
|
+
# it "updates the markup for a page with new markup (POST)" do
|
167
|
+
# new_markup = "# New markup\n\nThis is some *new* markup.\n"
|
168
|
+
# post '/patricia/edit', {:markup_url =>
|
169
|
+
# 'colors/red.md', :string => new_markup}
|
170
|
+
# expect(last_response).to be_ok
|
171
|
+
# end
|
172
|
+
end
|
173
|
+
|
145
174
|
end
|
146
175
|
|
147
176
|
describe "Use custom CSS and JavaScript" do
|
@@ -152,6 +181,7 @@ upon a GET request" do
|
|
152
181
|
:js_dir => File.join(File.dirname(__FILE__),
|
153
182
|
'assets/javascripts/'),
|
154
183
|
:tooltips => false,
|
184
|
+
:editor => false,
|
155
185
|
}
|
156
186
|
load_config(config)
|
157
187
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Cyan</title>
|
4
|
+
<link rel="stylesheet" href="" type="text/css" />
|
5
|
+
</head>
|
6
|
+
|
7
|
+
<body>
|
8
|
+
|
9
|
+
<h1>Color: Cyan</h1>
|
10
|
+
<h2>Description</h2>
|
11
|
+
<p>This is cyan.</p>
|
12
|
+
<hr />
|
13
|
+
<h2>Info</h2>
|
14
|
+
<p>Here is some info.</p>
|
15
|
+
|
16
|
+
<script type="text/javascript" src=""></script>
|
17
|
+
</body>
|
18
|
+
</html>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: patricia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- nounch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- lib/patricia/app.rb
|
58
58
|
- lib/patricia/assets/javascripts/app.js
|
59
59
|
- lib/patricia/assets/javascripts/bootstrap.min.js
|
60
|
+
- lib/patricia/assets/javascripts/editor.js
|
60
61
|
- lib/patricia/assets/javascripts/jquery-1.11.0.min.js
|
61
62
|
- lib/patricia/assets/javascripts/tooltips.js
|
62
63
|
- lib/patricia/assets/stylesheets/app.css
|
@@ -85,6 +86,7 @@ files:
|
|
85
86
|
- spec/random-test-wiki/amazing-animals/tall/giraffe.md
|
86
87
|
- spec/random-test-wiki/colors/blue.md
|
87
88
|
- spec/random-test-wiki/colors/bright-orange.org
|
89
|
+
- spec/random-test-wiki/colors/cyan.html
|
88
90
|
- spec/random-test-wiki/colors/dark-yellow.textile
|
89
91
|
- spec/random-test-wiki/colors/file.txt
|
90
92
|
- spec/random-test-wiki/colors/green.markdown
|
@@ -132,6 +134,7 @@ test_files:
|
|
132
134
|
- spec/random-test-wiki/amazing-animals/tall/giraffe.md
|
133
135
|
- spec/random-test-wiki/colors/blue.md
|
134
136
|
- spec/random-test-wiki/colors/bright-orange.org
|
137
|
+
- spec/random-test-wiki/colors/cyan.html
|
135
138
|
- spec/random-test-wiki/colors/dark-yellow.textile
|
136
139
|
- spec/random-test-wiki/colors/file.txt
|
137
140
|
- spec/random-test-wiki/colors/green.markdown
|