patricia 0.0.1 → 0.1.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 +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
|