├── .gitignore
├── .rubocop.yml
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.md
├── app
├── assets
│ ├── images
│ │ └── documentation
│ │ │ ├── link.svg
│ │ │ ├── logo-black.svg
│ │ │ ├── logo-white.svg
│ │ │ ├── page.svg
│ │ │ ├── pin.svg
│ │ │ ├── recommendation.svg
│ │ │ ├── search.svg
│ │ │ ├── unhappy.svg
│ │ │ └── warning.svg
│ ├── javascripts
│ │ └── documentation
│ │ │ ├── application.coffee
│ │ │ ├── jquery-ui.js
│ │ │ └── jquery.autosize.js
│ └── stylesheets
│ │ └── documentation
│ │ ├── application.scss
│ │ ├── markdown.scss
│ │ ├── page_form.scss
│ │ └── reset.scss
├── controllers
│ └── documentation
│ │ ├── application_controller.rb
│ │ └── pages_controller.rb
├── helpers
│ └── documentation
│ │ └── application_helper.rb
├── models
│ └── documentation
│ │ ├── page.rb
│ │ └── screenshot.rb
└── views
│ ├── documentation
│ ├── pages
│ │ ├── _admin_buttons.html.haml
│ │ ├── form.html.haml
│ │ ├── index.html.haml
│ │ ├── positioning.html.haml
│ │ ├── screenshot.html.haml
│ │ ├── search.html.haml
│ │ └── show.html.haml
│ └── shared
│ │ ├── access_denied.html.haml
│ │ └── not_found.html.haml
│ └── layouts
│ └── documentation
│ ├── _footer.html.haml
│ ├── _head.html.haml
│ ├── _header.html.haml
│ ├── _search.html.haml
│ └── application.html.haml
├── config
├── locales
│ └── en.yml
└── routes.rb
├── db
├── migrate
│ ├── 20140711185212_create_documentation_pages.rb
│ ├── 20140724111844_create_nifty_attachments_table.rb
│ └── 20140724114255_create_documentation_screenshots.rb
└── seeds.rb
├── doc
├── developers-guide
│ ├── authorization.md
│ ├── building-views
│ │ ├── accessing-pages.md
│ │ ├── helpers.md
│ │ └── overview.md
│ ├── customization.md
│ ├── overview.md
│ └── search-backends.md
└── markdown
│ └── overview.md
├── documentation.gemspec
├── lib
├── documentation.rb
├── documentation
│ ├── authorizer.rb
│ ├── config.rb
│ ├── engine.rb
│ ├── errors.rb
│ ├── generators
│ │ └── setup_generator.rb
│ ├── markdown_renderer.rb
│ ├── search_result.rb
│ ├── searchers
│ │ ├── abstract.rb
│ │ └── simple.rb
│ ├── version.rb
│ └── view_helpers.rb
└── tasks
│ └── documentation.rake
└── test
├── .gitignore
├── Rakefile
├── app
├── assets
│ ├── images
│ │ └── .keep
│ ├── javascripts
│ │ └── application.js
│ └── stylesheets
│ │ └── application.css
├── controllers
│ ├── application_controller.rb
│ └── concerns
│ │ └── .keep
├── helpers
│ └── application_helper.rb
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ └── concerns
│ │ └── .keep
└── views
│ └── layouts
│ └── application.html.erb
├── bin
├── bundle
├── rails
├── rake
└── spring
├── config.ru
├── config
├── application.rb
├── boot.rb
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── cookies_serializer.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
├── routes.rb
└── secrets.yml
├── db
└── schema.rb
├── lib
└── assets
│ └── .keep
├── log
└── .keep
└── public
├── 404.html
├── 422.html
├── 500.html
├── favicon.ico
└── robots.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | test/db/*.sqlite3
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from:
2 | - https://dev.k.io/rubocop.yml
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | gemspec
3 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | documentation (2.0.0)
5 | coffee-rails (>= 5)
6 | dynamic_form (~> 1.1.4)
7 | haml (>= 4.0)
8 | jquery-rails (>= 3.0)
9 | nifty-attachments (>= 1.0.3)
10 | nifty-dialog (~> 1)
11 | pygments.rb (>= 0.5)
12 | rails (>= 6.0, < 7.0)
13 | redcarpet (>= 3.1.0)
14 | sass-rails (>= 4.0)
15 | uglifier (>= 2.2)
16 |
17 | GEM
18 | remote: https://rubygems.org/
19 | specs:
20 | actioncable (6.0.2.2)
21 | actionpack (= 6.0.2.2)
22 | nio4r (~> 2.0)
23 | websocket-driver (>= 0.6.1)
24 | actionmailbox (6.0.2.2)
25 | actionpack (= 6.0.2.2)
26 | activejob (= 6.0.2.2)
27 | activerecord (= 6.0.2.2)
28 | activestorage (= 6.0.2.2)
29 | activesupport (= 6.0.2.2)
30 | mail (>= 2.7.1)
31 | actionmailer (6.0.2.2)
32 | actionpack (= 6.0.2.2)
33 | actionview (= 6.0.2.2)
34 | activejob (= 6.0.2.2)
35 | mail (~> 2.5, >= 2.5.4)
36 | rails-dom-testing (~> 2.0)
37 | actionpack (6.0.2.2)
38 | actionview (= 6.0.2.2)
39 | activesupport (= 6.0.2.2)
40 | rack (~> 2.0, >= 2.0.8)
41 | rack-test (>= 0.6.3)
42 | rails-dom-testing (~> 2.0)
43 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
44 | actiontext (6.0.2.2)
45 | actionpack (= 6.0.2.2)
46 | activerecord (= 6.0.2.2)
47 | activestorage (= 6.0.2.2)
48 | activesupport (= 6.0.2.2)
49 | nokogiri (>= 1.8.5)
50 | actionview (6.0.2.2)
51 | activesupport (= 6.0.2.2)
52 | builder (~> 3.1)
53 | erubi (~> 1.4)
54 | rails-dom-testing (~> 2.0)
55 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
56 | activejob (6.0.2.2)
57 | activesupport (= 6.0.2.2)
58 | globalid (>= 0.3.6)
59 | activemodel (6.0.2.2)
60 | activesupport (= 6.0.2.2)
61 | activerecord (6.0.2.2)
62 | activemodel (= 6.0.2.2)
63 | activesupport (= 6.0.2.2)
64 | activestorage (6.0.2.2)
65 | actionpack (= 6.0.2.2)
66 | activejob (= 6.0.2.2)
67 | activerecord (= 6.0.2.2)
68 | marcel (~> 0.3.1)
69 | activesupport (6.0.2.2)
70 | concurrent-ruby (~> 1.0, >= 1.0.2)
71 | i18n (>= 0.7, < 2)
72 | minitest (~> 5.1)
73 | tzinfo (~> 1.1)
74 | zeitwerk (~> 2.2)
75 | builder (3.2.4)
76 | coffee-rails (5.0.0)
77 | coffee-script (>= 2.2.0)
78 | railties (>= 5.2.0)
79 | coffee-script (2.4.1)
80 | coffee-script-source
81 | execjs
82 | coffee-script-source (1.12.2)
83 | concurrent-ruby (1.1.6)
84 | crass (1.0.6)
85 | dynamic_form (1.1.4)
86 | erubi (1.9.0)
87 | execjs (2.7.0)
88 | ffi (1.12.2)
89 | globalid (0.4.2)
90 | activesupport (>= 4.2.0)
91 | haml (5.1.2)
92 | temple (>= 0.8.0)
93 | tilt
94 | i18n (1.8.2)
95 | concurrent-ruby (~> 1.0)
96 | jquery-rails (4.3.5)
97 | rails-dom-testing (>= 1, < 3)
98 | railties (>= 4.2.0)
99 | thor (>= 0.14, < 2.0)
100 | loofah (2.5.0)
101 | crass (~> 1.0.2)
102 | nokogiri (>= 1.5.9)
103 | mail (2.7.1)
104 | mini_mime (>= 0.1.1)
105 | marcel (0.3.3)
106 | mimemagic (~> 0.3.2)
107 | method_source (1.0.0)
108 | mimemagic (0.3.4)
109 | mini_mime (1.0.2)
110 | mini_portile2 (2.4.0)
111 | minitest (5.14.0)
112 | multi_json (1.14.1)
113 | nifty-attachments (1.0.4)
114 | nifty-dialog (1.1.1)
115 | nio4r (2.5.2)
116 | nokogiri (1.10.9)
117 | mini_portile2 (~> 2.4.0)
118 | pygments.rb (1.2.1)
119 | multi_json (>= 1.0.0)
120 | rack (2.2.2)
121 | rack-test (1.1.0)
122 | rack (>= 1.0, < 3)
123 | rails (6.0.2.2)
124 | actioncable (= 6.0.2.2)
125 | actionmailbox (= 6.0.2.2)
126 | actionmailer (= 6.0.2.2)
127 | actionpack (= 6.0.2.2)
128 | actiontext (= 6.0.2.2)
129 | actionview (= 6.0.2.2)
130 | activejob (= 6.0.2.2)
131 | activemodel (= 6.0.2.2)
132 | activerecord (= 6.0.2.2)
133 | activestorage (= 6.0.2.2)
134 | activesupport (= 6.0.2.2)
135 | bundler (>= 1.3.0)
136 | railties (= 6.0.2.2)
137 | sprockets-rails (>= 2.0.0)
138 | rails-dom-testing (2.0.3)
139 | activesupport (>= 4.2.0)
140 | nokogiri (>= 1.6)
141 | rails-html-sanitizer (1.3.0)
142 | loofah (~> 2.3)
143 | railties (6.0.2.2)
144 | actionpack (= 6.0.2.2)
145 | activesupport (= 6.0.2.2)
146 | method_source
147 | rake (>= 0.8.7)
148 | thor (>= 0.20.3, < 2.0)
149 | rake (13.0.1)
150 | redcarpet (3.5.0)
151 | sass-rails (6.0.0)
152 | sassc-rails (~> 2.1, >= 2.1.1)
153 | sassc (2.3.0)
154 | ffi (~> 1.9)
155 | sassc-rails (2.1.2)
156 | railties (>= 4.0.0)
157 | sassc (>= 2.0)
158 | sprockets (> 3.0)
159 | sprockets-rails
160 | tilt
161 | sprockets (4.0.0)
162 | concurrent-ruby (~> 1.0)
163 | rack (> 1, < 3)
164 | sprockets-rails (3.2.1)
165 | actionpack (>= 4.0)
166 | activesupport (>= 4.0)
167 | sprockets (>= 3.0.0)
168 | sqlite3 (1.3.9)
169 | temple (0.8.2)
170 | thor (1.0.1)
171 | thread_safe (0.3.6)
172 | tilt (2.0.10)
173 | tzinfo (1.2.7)
174 | thread_safe (~> 0.1)
175 | uglifier (4.2.0)
176 | execjs (>= 0.3.0, < 3)
177 | websocket-driver (0.7.1)
178 | websocket-extensions (>= 0.1.0)
179 | websocket-extensions (0.1.4)
180 | zeitwerk (2.3.0)
181 |
182 | PLATFORMS
183 | ruby
184 |
185 | DEPENDENCIES
186 | documentation!
187 | sqlite3 (~> 1.3)
188 |
189 | BUNDLED WITH
190 | 1.17.2
191 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2014 Adam Cooke.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | Documentation is a Rails engine which provides a complete system for managing a set of hierarchical documentation. Once installed in an application, you'll have a complete user interface for viewing as well as editing a set of markdown pages.
4 |
5 | 
6 |
7 | ## Installation
8 |
9 | To get started, you need to add Documentation to your Gemfile and run `bundle`.
10 |
11 | ```ruby
12 | gem 'documentation', '~> 1.0.0'
13 | ```
14 |
15 | [](http://badge.fury.io/rb/documentation)
16 |
17 | Next, you'll need to run the setup generator which will add a route to your `config/routes.rb` file for the Documentation interface.
18 |
19 | ```
20 | bundle exec rails generate documentation:setup
21 | ```
22 |
23 | You now need to populate your database schema and load the initial documentation pages.
24 |
25 | ```
26 | bundle exec rake db:migrate documentation:install_guides
27 | ```
28 |
29 | Once this is done, you can go ahead and start up your Rails application and browse to `/docs` to view your documentation system.
30 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
29 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/logo-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
24 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
24 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/page.svg:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/pin.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/recommendation.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/unhappy.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/app/assets/images/documentation/warning.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/documentation/application.coffee:
--------------------------------------------------------------------------------
1 | #= require jquery
2 | #= require jquery_ujs
3 | #= require documentation/jquery-ui
4 | #= require documentation/jquery.autosize
5 | #= require nifty/dialog
6 |
7 | $ ->
8 | $('form.pageForm textarea').autosize({append: '\n\n'})
9 | $('form.reordering ul').sortable
10 | containment: 'parent'
11 | update: ->
12 | form = $(this).parents('form')
13 | url = form.attr('action')
14 | $.post(url, form.serialize());
15 |
16 | $('.js-screenshot').on 'click', (e)->
17 | e.preventDefault()
18 | openUploadDialog()
19 |
20 | $('.edit-article').on 'drop', (e)->
21 | e.stopPropagation()
22 | e.preventDefault()
23 |
24 | file = e.originalEvent.dataTransfer.files[0]
25 |
26 | openUploadDialog(file)
27 |
28 | openUploadDialog = (file)->
29 | editForm = $('.edit-article').find('form')
30 | contentArea = editForm.find('#page_content')
31 | caretPosition = contentArea.get(0).selectionStart
32 | content = contentArea.val()
33 |
34 | uploadFormUrl = editForm.find('.js-screenshot').attr('href')
35 | uploadFormUrl = uploadFormUrl + "?filename=#{file.name}" if file?
36 |
37 | Nifty.Dialog.open
38 | url: uploadFormUrl
39 | afterLoad: (dialog)->
40 | form = dialog.find('form')
41 | form.find('#screenshot_alt_text').focus()
42 |
43 | dialog.on 'submit', 'form', (e)->
44 | form = $(this)
45 | formData = new FormData(form.get(0))
46 |
47 | if file?
48 | filename = file.name
49 | formData.append('screenshot[upload_file]', file)
50 | else
51 | filename = form.find('#screenshot_upload_file').val()
52 |
53 | fileExt = filename.split('.').pop().toLowerCase()
54 | if $.inArray(fileExt, ['gif','png','jpg','jpeg']) == -1
55 | alert("Invalid file extension (allowed: gif, png, jpg, jpeg)")
56 | return false
57 |
58 | $.ajax
59 | url: form.attr('action')
60 | type: form.attr('method')
61 | data: formData
62 | processData: false
63 | contentType: false
64 | success: (data)->
65 | screenshotLink = ""
66 | newContent = content.substring(0, caretPosition) + screenshotLink + content.substring(caretPosition)
67 | newCaretPosition = (content.substring(0, caretPosition) + screenshotLink).length
68 | contentArea.val(newContent)
69 |
70 | contentArea.focus()
71 | contentArea.get(0).setSelectionRange(newCaretPosition, newCaretPosition)
72 |
73 | Nifty.Dialog.closeTopDialog()
74 |
75 | error: (xhr, status, errorThrown)->
76 | console.log xhr.responseText
77 |
78 | false
79 |
--------------------------------------------------------------------------------
/app/assets/javascripts/documentation/jquery.autosize.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Autosize v1.18.4 - 2014-01-11
3 | Automatically adjust textarea height based on user input.
4 | (c) 2014 Jack Moore - http://www.jacklmoore.com/autosize
5 | license: http://www.opensource.org/licenses/mit-license.php
6 | */
7 | (function ($) {
8 | var
9 | defaults = {
10 | className: 'autosizejs',
11 | append: '',
12 | callback: false,
13 | resizeDelay: 10,
14 | placeholder: true
15 | },
16 |
17 | // border:0 is unnecessary, but avoids a bug in Firefox on OSX
18 | copy = '',
19 |
20 | // line-height is conditionally included because IE7/IE8/old Opera do not return the correct value.
21 | typographyStyles = [
22 | 'fontFamily',
23 | 'fontSize',
24 | 'fontWeight',
25 | 'fontStyle',
26 | 'letterSpacing',
27 | 'textTransform',
28 | 'wordSpacing',
29 | 'textIndent'
30 | ],
31 |
32 | // to keep track which textarea is being mirrored when adjust() is called.
33 | mirrored,
34 |
35 | // the mirror element, which is used to calculate what size the mirrored element should be.
36 | mirror = $(copy).data('autosize', true)[0];
37 |
38 | // test that line-height can be accurately copied.
39 | mirror.style.lineHeight = '99px';
40 | if ($(mirror).css('lineHeight') === '99px') {
41 | typographyStyles.push('lineHeight');
42 | }
43 | mirror.style.lineHeight = '';
44 |
45 | $.fn.autosize = function (options) {
46 | if (!this.length) {
47 | return this;
48 | }
49 |
50 | options = $.extend({}, defaults, options || {});
51 |
52 | if (mirror.parentNode !== document.body) {
53 | $(document.body).append(mirror);
54 | }
55 |
56 | return this.each(function () {
57 | var
58 | ta = this,
59 | $ta = $(ta),
60 | maxHeight,
61 | minHeight,
62 | boxOffset = 0,
63 | callback = $.isFunction(options.callback),
64 | originalStyles = {
65 | height: ta.style.height,
66 | overflow: ta.style.overflow,
67 | overflowY: ta.style.overflowY,
68 | wordWrap: ta.style.wordWrap,
69 | resize: ta.style.resize
70 | },
71 | timeout,
72 | width = $ta.width();
73 |
74 | if ($ta.data('autosize')) {
75 | // exit if autosize has already been applied, or if the textarea is the mirror element.
76 | return;
77 | }
78 | $ta.data('autosize', true);
79 |
80 | if ($ta.css('box-sizing') === 'border-box' || $ta.css('-moz-box-sizing') === 'border-box' || $ta.css('-webkit-box-sizing') === 'border-box'){
81 | boxOffset = $ta.outerHeight() - $ta.height();
82 | }
83 |
84 | // IE8 and lower return 'auto', which parses to NaN, if no min-height is set.
85 | minHeight = Math.max(parseInt($ta.css('minHeight'), 10) - boxOffset || 0, $ta.height());
86 |
87 | $ta.css({
88 | overflow: 'hidden',
89 | overflowY: 'hidden',
90 | wordWrap: 'break-word', // horizontal overflow is hidden, so break-word is necessary for handling words longer than the textarea width
91 | resize: ($ta.css('resize') === 'none' || $ta.css('resize') === 'vertical') ? 'none' : 'horizontal'
92 | });
93 |
94 | // The mirror width must exactly match the textarea width, so using getBoundingClientRect because it doesn't round the sub-pixel value.
95 | // window.getComputedStyle, getBoundingClientRect returning a width are unsupported, but also unneeded in IE8 and lower.
96 | function setWidth() {
97 | var width;
98 | var style = window.getComputedStyle ? window.getComputedStyle(ta, null) : false;
99 |
100 | if (style) {
101 |
102 | width = ta.getBoundingClientRect().width;
103 |
104 | if (width === 0) {
105 | width = parseInt(style.width,10);
106 | }
107 |
108 | $.each(['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'], function(i,val){
109 | width -= parseInt(style[val],10);
110 | });
111 | } else {
112 | width = Math.max($ta.width(), 0);
113 | }
114 |
115 | mirror.style.width = width + 'px';
116 | }
117 |
118 | function initMirror() {
119 | var styles = {};
120 |
121 | mirrored = ta;
122 | mirror.className = options.className;
123 | maxHeight = parseInt($ta.css('maxHeight'), 10);
124 |
125 | // mirror is a duplicate textarea located off-screen that
126 | // is automatically updated to contain the same text as the
127 | // original textarea. mirror always has a height of 0.
128 | // This gives a cross-browser supported way getting the actual
129 | // height of the text, through the scrollTop property.
130 | $.each(typographyStyles, function(i,val){
131 | styles[val] = $ta.css(val);
132 | });
133 | $(mirror).css(styles);
134 |
135 | setWidth();
136 |
137 | // Chrome-specific fix:
138 | // When the textarea y-overflow is hidden, Chrome doesn't reflow the text to account for the space
139 | // made available by removing the scrollbar. This workaround triggers the reflow for Chrome.
140 | if (window.chrome) {
141 | var width = ta.style.width;
142 | ta.style.width = '0px';
143 | var ignore = ta.offsetWidth;
144 | ta.style.width = width;
145 | }
146 | }
147 |
148 | // Using mainly bare JS in this function because it is going
149 | // to fire very often while typing, and needs to very efficient.
150 | function adjust() {
151 | var height, original;
152 |
153 | if (mirrored !== ta) {
154 | initMirror();
155 | } else {
156 | setWidth();
157 | }
158 |
159 | if (!ta.value && options.placeholder) {
160 | // If the textarea is empty, copy the placeholder text into
161 | // the mirror control and use that for sizing so that we
162 | // don't end up with placeholder getting trimmed.
163 | mirror.value = ($(ta).attr("placeholder") || '') + options.append;
164 | } else {
165 | mirror.value = ta.value + options.append;
166 | }
167 |
168 | mirror.style.overflowY = ta.style.overflowY;
169 | original = parseInt(ta.style.height,10);
170 |
171 | // Setting scrollTop to zero is needed in IE8 and lower for the next step to be accurately applied
172 | mirror.scrollTop = 0;
173 |
174 | mirror.scrollTop = 9e4;
175 |
176 | // Using scrollTop rather than scrollHeight because scrollHeight is non-standard and includes padding.
177 | height = mirror.scrollTop;
178 |
179 | if (maxHeight && height > maxHeight) {
180 | ta.style.overflowY = 'scroll';
181 | height = maxHeight;
182 | } else {
183 | ta.style.overflowY = 'hidden';
184 | if (height < minHeight) {
185 | height = minHeight;
186 | }
187 | }
188 |
189 | height += boxOffset;
190 |
191 | if (original !== height) {
192 | ta.style.height = height + 'px';
193 | if (callback) {
194 | options.callback.call(ta,ta);
195 | }
196 | }
197 | }
198 |
199 | function resize () {
200 | clearTimeout(timeout);
201 | timeout = setTimeout(function(){
202 | var newWidth = $ta.width();
203 |
204 | if (newWidth !== width) {
205 | width = newWidth;
206 | adjust();
207 | }
208 | }, parseInt(options.resizeDelay,10));
209 | }
210 |
211 | if ('onpropertychange' in ta) {
212 | if ('oninput' in ta) {
213 | // Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
214 | // so binding to onkeyup to catch most of those occasions. There is no way that I
215 | // know of to detect something like 'cut' in IE9.
216 | $ta.on('input.autosize keyup.autosize', adjust);
217 | } else {
218 | // IE7 / IE8
219 | $ta.on('propertychange.autosize', function(){
220 | if(event.propertyName === 'value'){
221 | adjust();
222 | }
223 | });
224 | }
225 | } else {
226 | // Modern Browsers
227 | $ta.on('input.autosize', adjust);
228 | }
229 |
230 | // Set options.resizeDelay to false if using fixed-width textarea elements.
231 | // Uses a timeout and width check to reduce the amount of times adjust needs to be called after window resize.
232 |
233 | if (options.resizeDelay !== false) {
234 | $(window).on('resize.autosize', resize);
235 | }
236 |
237 | // Event for manual triggering if needed.
238 | // Should only be needed when the value of the textarea is changed through JavaScript rather than user input.
239 | $ta.on('autosize.resize', adjust);
240 |
241 | // Event for manual triggering that also forces the styles to update as well.
242 | // Should only be needed if one of typography styles of the textarea change, and the textarea is already the target of the adjust method.
243 | $ta.on('autosize.resizeIncludeStyle', function() {
244 | mirrored = null;
245 | adjust();
246 | });
247 |
248 | $ta.on('autosize.destroy', function(){
249 | mirrored = null;
250 | clearTimeout(timeout);
251 | $(window).off('resize', resize);
252 | $ta
253 | .off('autosize')
254 | .off('.autosize')
255 | .css(originalStyles)
256 | .removeData('autosize');
257 | });
258 |
259 | // Call adjust in case the textarea already contains text.
260 | adjust();
261 | });
262 | };
263 | }(window.jQuery || window.$)); // jQuery or jQuery-like library, such as Zepto
264 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/documentation/application.scss:
--------------------------------------------------------------------------------
1 | /*
2 | *= require documentation/reset
3 | *= require nifty/dialog
4 | *= require_self
5 | *= require documentation/markdown
6 | *= require documentation/page_form
7 | */
8 |
9 | $font: "Helvetica Neue", Helvetica, Arial, sans-serif;
10 | input,
11 | textarea {
12 | font-family: $font;
13 | }
14 |
15 | body {
16 | font-family: $font;
17 | font-size: 14px;
18 | }
19 |
20 | //
21 | // Main page header
22 | //
23 | header {
24 | background: #354050;
25 | height: 45px;
26 | position: fixed;
27 | top: 0;
28 | width: 100%;
29 | h1 {
30 | margin: 10px 0 0 15px;
31 | float: left;
32 | a {
33 | display: block;
34 | background: image-url("documentation/logo-white.svg") no-repeat;
35 | background-size: 20px;
36 | width: 20px;
37 | height: 20px;
38 | text-indent: -40000px;
39 | }
40 | }
41 | p.back {
42 | float: right;
43 | margin: 14px 15px 0 0;
44 | a {
45 | color: #97a4b7;
46 | text-decoration: none;
47 | }
48 | a:hover {
49 | color: #fff;
50 | }
51 | a + a {
52 | margin-left: 15px;
53 | }
54 | }
55 | }
56 |
57 | //
58 | // Search Form
59 | //
60 | .searchForm {
61 | padding: 0 15px;
62 | margin-bottom: 15px;
63 | input {
64 | width: 100%;
65 | background: image-url("documentation/search.svg") white no-repeat 5px 6px;
66 | font-family: $font;
67 | background-size: 14px;
68 | border: 2px solid #bbc5da;
69 | border-radius: 4px;
70 | padding: 6px 6px 6px 25px;
71 | }
72 | }
73 |
74 | //
75 | // Left-side navigation
76 | //
77 | nav.pages {
78 | position: fixed;
79 | top: 45px;
80 | bottom: 0;
81 | width: 249px;
82 | border-right: 1px solid #e2e7f1;
83 | background: #f5f7fb;
84 | font-size: 0.9em;
85 | div.inner {
86 | padding: 15px 0;
87 | ul {
88 | li {
89 | a {
90 | text-decoration: none;
91 | color: #333;
92 | padding: 5px 15px;
93 | display: block;
94 | &:hover {
95 | background: #e2e7f1;
96 | }
97 | &.active {
98 | background: #35a4d4;
99 | color: #fff;
100 | }
101 | }
102 | ul li a {
103 | padding-left: 35px;
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
110 | //
111 | // Main content area
112 | //
113 | section.content {
114 | position: fixed;
115 | top: 45px;
116 | left: 250px;
117 | bottom: 0;
118 | right: 0;
119 | background: #fff;
120 | overflow-y: scroll;
121 | }
122 |
123 | //
124 | // The breadcrumb
125 | //
126 | nav.breadcrumb {
127 | font-size: 0.85em;
128 | background: #e2e7f1;
129 | margin-bottom: 10px;
130 | padding: 10px 35px;
131 | ul {
132 | overflow: hidden;
133 | li {
134 | float: left;
135 | margin-right: 7px;
136 | &:after {
137 | content: "\21D2";
138 | padding-left: 7px;
139 | color: #abb6cd;
140 | }
141 | &:last-child:after {
142 | color: #e2e7f1;
143 | padding-left: 0;
144 | }
145 | a {
146 | color: #8e9bb4;
147 | }
148 | a:hover {
149 | color: #748098;
150 | }
151 | &:last-child a {
152 | text-decoration: none;
153 | }
154 | &:last-child a:hover {
155 | color: #8e9bb4;
156 | }
157 | }
158 | }
159 | }
160 |
161 | //
162 | // A page
163 | //
164 | section.page {
165 | margin: 25px 35px 35px 35px;
166 | h1 {
167 | font-size: 2.2em;
168 | font-weight: 300;
169 | margin-bottom: 4px;
170 | }
171 |
172 | p.updated {
173 | color: #999;
174 | font-size: 0.85em;
175 | margin-bottom: 10px;
176 | }
177 |
178 | p.previewLink {
179 | float: right;
180 | margin-top: 4px;
181 | }
182 |
183 | p.adminButtons {
184 | border-top: 1px dashed #ddd;
185 | padding: 15px 0;
186 | margin-top: 30px;
187 | text-align: right;
188 | }
189 |
190 | form.reordering {
191 | padding: 0 0 0 0;
192 | }
193 | form.reordering ul {
194 | padding: 25px 0 0 0;
195 | }
196 | form.reordering ul li {
197 | background: #f5f7fb;
198 | font-size: 1.1em;
199 | margin-bottom: 7px;
200 | padding: 10px;
201 | cursor: pointer;
202 | border: 1px dashed #748098;
203 | background: image-url("documentation/page.svg") #f5f7fb no-repeat 16px 10px;
204 | background-size: 14px;
205 | padding-left: 40px;
206 | }
207 | }
208 |
209 | //
210 | // Generic button style
211 | //
212 | a.button,
213 | input.button {
214 | display: inline-block;
215 | border: 2px solid lighten(#9357b0, 20%);
216 | text-decoration: none;
217 | background: #fff;
218 | color: #9357b0;
219 | font-weight: 500;
220 | padding: 3px 10px;
221 | font-size: 12px;
222 | background: #fff;
223 | border-radius: 5px;
224 | margin-left: 5px;
225 | appearance: none;
226 | vertical-align: middle;
227 | &.delete {
228 | color: #f6744f;
229 | border-color: lighten(#f6744f, 20%);
230 | }
231 | &.new {
232 | color: #65ba65;
233 | border-color: lighten(#65ba65, 20%);
234 | }
235 | &.preview {
236 | color: #999;
237 | border-color: #ccc;
238 | }
239 | }
240 |
241 | //
242 | // Flash message - for notices
243 | //
244 | #flash-notice {
245 | background: #65ba65;
246 | padding: 15px 35px;
247 | color: #fff;
248 | font-size: 1.2em;
249 | font-weight: 600;
250 | }
251 |
252 | //
253 | // Flash message - for alerts
254 | //
255 | #flash-alert {
256 | background: #f6744f;
257 | padding: 15px 35px;
258 | color: #fff;
259 | font-size: 1.2em;
260 | font-weight: 600;
261 | }
262 |
263 | //
264 | // Welcome message on homepage
265 | //
266 | section.welcome {
267 | text-align: center;
268 | margin: 60px 0;
269 | background: image-url("documentation/logo-black.svg") no-repeat top center;
270 | background-size: 100px;
271 | padding-top: 150px;
272 | h1 {
273 | font-size: 3.6em;
274 | font-weight: 300;
275 | }
276 | h2 {
277 | color: #aaa;
278 | margin-top: 2px;
279 | margin-bottom: 25px;
280 | font-size: 1.5em;
281 | font-weight: 300;
282 | }
283 | .override {
284 | background: #fdffe0;
285 | width: 80%;
286 | margin: 50px auto;
287 | font-size: 1.1em;
288 | line-height: 1.7;
289 | padding: 25px;
290 | border: 1px solid #ced0ad;
291 | color: #727457;
292 | code {
293 | font-weight: bold;
294 | }
295 | }
296 | }
297 |
298 | //
299 | // Access denied
300 | //
301 | html.errorPage {
302 | background: lighten(#354050, 70%);
303 | }
304 |
305 | section.errorMessage {
306 | width: 450px;
307 | margin: 100px auto;
308 | box-shadow: 0 0 35px lighten(#354050, 60%);
309 | background: white;
310 | text-align: center;
311 | h2 {
312 | background: #354050;
313 | color: #fff;
314 | padding: 15px;
315 | font-size: 1.5em;
316 | font-weight: 300;
317 | }
318 | p {
319 | padding: 25px 15px;
320 | line-height: 1.5;
321 | font-size: 0.9em;
322 | color: lighten(#354050, 30%);
323 | }
324 | }
325 |
326 | //
327 | // Search results
328 | //
329 | ul.searchResults {
330 | margin: 25px 0;
331 | a {
332 | color: inherit;
333 | text-decoration: none;
334 | }
335 | a:hover {
336 | text-decoration: underline;
337 | }
338 | li {
339 | margin-bottom: 25px;
340 | h4 {
341 | font-weight: 600;
342 | color: #35a4d4;
343 | }
344 | p.in {
345 | font-size: 0.85em;
346 | color: #92bbc9;
347 | margin-top: 4px;
348 | }
349 | p.excerpt {
350 | margin-top: 5px;
351 | line-height: 1.5;
352 | }
353 | p.excerpt mark {
354 | background: #fffdca;
355 | border-bottom: 1px solid #d9d33b;
356 | }
357 | }
358 | }
359 |
360 | //
361 | // Search summary
362 | //
363 | p.searchSummary {
364 | color: #999;
365 | margin-top: 5px;
366 | }
367 |
368 | //
369 | // No search results
370 | //
371 | p.noSearchResults {
372 | text-align: center;
373 | margin: 100px 0;
374 | color: #999;
375 | font-style: italic;
376 | font-size: 1.5em;
377 | font-weight: 300;
378 | background: image-url("documentation/unhappy.svg") no-repeat center top;
379 | background-size: 170px;
380 | padding-top: 200px;
381 | }
382 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/documentation/markdown.scss:
--------------------------------------------------------------------------------
1 | div.documentationMarkdown {
2 | a { color:#35A4D4; text-decoration:none; border-bottom:1px solid #35A4D4; font-weight:600;}
3 |
4 | h1, h2 { font-size:1.6em; font-weight:300; margin:20px 0; border-bottom:1px solid #aecdda; padding-bottom:2px; color:#35A4D4;}
5 | h3 { font-size:1.3em; font-weight:500; margin:20px 0; }
6 | h4 { font-size:1.1em; font-weight:500; margin:20px 0;}
7 | h5 { font-size:1.1em; font-weight:500; margin:20px 0;}
8 |
9 | p { margin:20px 0; line-height:1.8;}
10 |
11 | ul, ol { line-height:1.8em; margin:20px 0; margin-left:40px; }
12 | ul li { background:image-url('documentation/pin.svg') no-repeat 0 5px; background-size:13px; padding-left:25px;}
13 | ol { margin-left:65px;}
14 | ol li { margin-bottom:8px;padding-left:15px;}
15 | ol li:last-child { margin-bottom:0; }
16 | ul.pages {
17 | overflow:hidden;
18 | li { background:image-url('documentation/page.svg') no-repeat 0 5px; background-size:13px; padding-left:25px; width:45%; float:left; margin-bottom:5px;}
19 | }
20 |
21 | h2, h3, h4, h5 {
22 | &:hover a.anchor { display:inline-block;}
23 | }
24 |
25 | a.anchor { display:none; float:right; border:0; color:#999; width:16px; height:16px; background:image-url('documentation/link.svg'); opacity:0.6; background-size:16px; text-indent:-40000px;}
26 |
27 | p.recommendation, p.notice {
28 | background:image-url('documentation/recommendation.svg') #F5F7FB no-repeat 20px 16px;
29 | background-size:20px;
30 | border:1px solid #E4E7ED;
31 | padding:15px;
32 | margin-left:0;
33 | margin-right:0;
34 | padding-left:55px;
35 | }
36 |
37 | p.warning {
38 | background:image-url('documentation/warning.svg') #fffbec no-repeat 20px 18px;
39 | background-size:20px;
40 | border:1px solid #edda8c;
41 | padding:15px;
42 | color:#97894f;
43 | margin-left:0;
44 | margin-right:0;
45 | padding-left:55px;
46 | }
47 |
48 | p.codeTitle {
49 | background:#f7f7f7;
50 | margin-bottom:0;
51 | line-height:1;
52 | border-top-left-radius:6px;
53 | border-top-right-radius:6px;
54 | padding:15px 20px;
55 | font-size:0.9em;
56 | font-weight:500;
57 | color:#666;
58 | border:1px solid #ddd;
59 | border-bottom:0;
60 | }
61 |
62 | p.codeTitle + div.highlight pre {
63 | margin-top:0 !important;
64 | border-top-left-radius:0;
65 | border-top-right-radius:0;
66 | }
67 |
68 | pre {
69 | margin:25px 0;
70 | line-height:1.4;
71 | padding:20px;
72 | border-radius:6px;
73 | overflow-x:auto;
74 | word-wrap: normal;
75 | white-space: pre;
76 | }
77 |
78 | code { background:#F8F8F8; border:1px solid #ddd; border-radius:4px; padding:0px 5px; margin:0 2px; font-size:0.85em; white-space:pre; word-wrap:none;}
79 |
80 | .imgcontainer.center {
81 | display:block;
82 | margin:0 35px;
83 | text-align:center;
84 | img {
85 | max-width:90%;
86 | }
87 | }
88 |
89 | table {
90 | margin:25px 0;
91 | min-width:100%;
92 | td, th { border:1px solid #E4E7ED; padding:10px;}
93 | th { background:#F5F7FB; color:#8E9BB4; font-weight:300; text-align:left;}
94 | }
95 |
96 | .highlight {
97 | pre {
98 | font-family:Consolas, "Liberation Mono", Courier, monospace;
99 | font-size:15px;
100 | background: #1d1f21; color: #c5c8c6;
101 | .hll { background-color: #373b41 }
102 | .c { color: #969896 } /* Comment */
103 | .err { color: #cc6666 } /* Error */
104 | .k { color: #b294bb } /* Keyword */
105 | .l { color: #de935f } /* Literal */
106 | .n { color: #c5c8c6 } /* Name */
107 | .o { color: #8abeb7 } /* Operator */
108 | .p { color: #c5c8c6 } /* Punctuation */
109 | .cm { color: #969896 } /* Comment.Multiline */
110 | .cp { color: #969896 } /* Comment.Preproc */
111 | .c1 { color: #969896 } /* Comment.Single */
112 | .cs { color: #969896 } /* Comment.Special */
113 | .gd { color: #cc6666 } /* Generic.Deleted */
114 | .ge { font-style: italic } /* Generic.Emph */
115 | .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
116 | .gi { color: #b5bd68 } /* Generic.Inserted */
117 | .gp { color: #969896; font-weight: bold } /* Generic.Prompt */
118 | .gs { font-weight: bold } /* Generic.Strong */
119 | .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */
120 | .kc { color: #b294bb } /* Keyword.Constant */
121 | .kd { color: #b294bb } /* Keyword.Declaration */
122 | .kn { color: #8abeb7 } /* Keyword.Namespace */
123 | .kp { color: #b294bb } /* Keyword.Pseudo */
124 | .kr { color: #b294bb } /* Keyword.Reserved */
125 | .kt { color: #f0c674 } /* Keyword.Type */
126 | .ld { color: #b5bd68 } /* Literal.Date */
127 | .m { color: #de935f } /* Literal.Number */
128 | .s { color: #b5bd68 } /* Literal.String */
129 | .na { color: #81a2be } /* Name.Attribute */
130 | .nb { color: #c5c8c6 } /* Name.Builtin */
131 | .nc { color: #f0c674 } /* Name.Class */
132 | .no { color: #cc6666 } /* Name.Constant */
133 | .nd { color: #8abeb7 } /* Name.Decorator */
134 | .ni { color: #c5c8c6 } /* Name.Entity */
135 | .ne { color: #cc6666 } /* Name.Exception */
136 | .nf { color: #81a2be } /* Name.Function */
137 | .nl { color: #c5c8c6 } /* Name.Label */
138 | .nn { color: #f0c674 } /* Name.Namespace */
139 | .nx { color: #81a2be } /* Name.Other */
140 | .py { color: #c5c8c6 } /* Name.Property */
141 | .nt { color: #8abeb7 } /* Name.Tag */
142 | .nv { color: #cc6666 } /* Name.Variable */
143 | .ow { color: #8abeb7 } /* Operator.Word */
144 | .w { color: #c5c8c6 } /* Text.Whitespace */
145 | .mf { color: #de935f } /* Literal.Number.Float */
146 | .mh { color: #de935f } /* Literal.Number.Hex */
147 | .mi { color: #de935f } /* Literal.Number.Integer */
148 | .mo { color: #de935f } /* Literal.Number.Oct */
149 | .sb { color: #b5bd68 } /* Literal.String.Backtick */
150 | .sc { color: #c5c8c6 } /* Literal.String.Char */
151 | .sd { color: #969896 } /* Literal.String.Doc */
152 | .s2 { color: #b5bd68 } /* Literal.String.Double */
153 | .se { color: #de935f } /* Literal.String.Escape */
154 | .sh { color: #b5bd68 } /* Literal.String.Heredoc */
155 | .si { color: #de935f } /* Literal.String.Interpol */
156 | .sx { color: #b5bd68 } /* Literal.String.Other */
157 | .sr { color: #b5bd68 } /* Literal.String.Regex */
158 | .s1 { color: #b5bd68 } /* Literal.String.Single */
159 | .ss { color: #b5bd68 } /* Literal.String.Symbol */
160 | .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
161 | .vc { color: #cc6666 } /* Name.Variable.Class */
162 | .vg { color: #cc6666 } /* Name.Variable.Global */
163 | .vi { color: #cc6666 } /* Name.Variable.Instance */
164 | .il { color: #de935f } /* Literal.Number.Integer.Long */
165 | }
166 | }
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/documentation/page_form.scss:
--------------------------------------------------------------------------------
1 | form.pageForm {
2 | padding:15px 35px 25px 35px;
3 | margin-top:10px;
4 | input[type=text], textarea {
5 | width:100%;
6 | border:1px solid #c7cfde;
7 | padding:6px;
8 | background:#F5F7FB;
9 | }
10 |
11 | p.title {
12 | margin-bottom:15px;
13 | input { font-size:1.3em; font-weight:normal; padding:10px;}
14 | }
15 |
16 | p.content {
17 | textarea {
18 | width:100%;
19 | min-height:300px;
20 | font-size:0.9em;
21 | line-height:1.4;
22 | padding:10px;
23 | font-family:'Consolas', Monaco, 'Courier New', Courier, fixed;
24 | resize:none;
25 | }
26 | }
27 |
28 | dl {
29 | margin:15px 0;
30 | dt { width:120px; float:left; color:#999;padding-top:6px;}
31 | dd { margin-left:160px; }
32 | dd input { width:100%; font-size:1.1em;}
33 | dd.padded { padding-top: 6px; }
34 | }
35 |
36 | p.submit {
37 | text-align:right;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/documentation/reset.scss:
--------------------------------------------------------------------------------
1 | html, body, body div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, figure, footer, header, hgroup, menu, nav, section, time, mark, audio, video {
2 | margin: 0;
3 | padding: 0;
4 | border: 0;
5 | outline: 0;
6 | font-size: 100%;
7 | letter-spacing:0;
8 | vertical-align: baseline;
9 | background: transparent;
10 | font-weight:normal;
11 | }
12 |
13 | article, aside, figure, footer, header, hgroup, nav, section {display: block;}
14 |
15 | img,object,embed {max-width: 100%;}
16 | ul {list-style: none;}
17 | blockquote, q {quotes: none;}
18 | b,strong { font-weight: bold;}
19 | blockquote:before, blockquote:after, q:before, q:after {content: ''; content: none;}
20 |
21 | a {margin: 0; padding: 0; font-size: 100%; vertical-align: baseline; background: transparent;}
22 |
23 | del {text-decoration: line-through;}
24 |
25 | abbr[title], dfn[title] {border-bottom: 1px dotted #000; cursor: help;}
26 |
27 | /* tables still need cellspacing="0" in the markup */
28 | table {border-collapse: collapse; border-spacing: 0;}
29 | th {font-weight: bold; vertical-align: bottom;}
30 | td {font-weight: normal; vertical-align: top;}
31 |
32 | hr {display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0;}
33 |
34 | input, select {vertical-align: middle;}
35 |
36 | pre {
37 | white-space: pre; /* CSS2 */
38 | white-space: pre-wrap; /* CSS 2.1 */
39 | white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
40 | word-wrap: break-word; /* IE */
41 | }
42 |
43 | input[type="radio"] {vertical-align: text-bottom;}
44 | input[type="checkbox"] {vertical-align: bottom; *vertical-align: baseline;}
45 | .ie6 input {vertical-align: text-bottom;}
46 |
47 | select, input, textarea {font: 99% sans-serif;}
48 |
49 | table {font-size: inherit; font: 100%;}
50 |
51 | /* Accessible focus treatment
52 | people.opera.com/patrickl/experiments/keyboard/test */
53 | a:hover, a:active {outline: none;}
54 |
55 | small {font-size: 85%;}
56 |
57 | strong, th {font-weight: bold;}
58 |
59 | td, td img {vertical-align: top;}
60 |
61 | /* Make sure sup and sub don't screw with your line-heights
62 | gist.github.com/413930 */
63 | sub, sup {font-size: 75%; line-height: 0; position: relative;}
64 | sup {top: -0.5em;}
65 | sub {bottom: -0.25em;}
66 |
67 | /* standardize any monospaced elements */
68 | pre, code, kbd, samp {font-family: monospace, sans-serif;}
69 |
70 | /* hand cursor on clickable elements */
71 | .clickable,
72 | label,
73 | input[type=button],
74 | input[type=submit],
75 | button {cursor: pointer;}
76 |
77 | /* Webkit browsers add a 2px margin outside the chrome of form elements */
78 | button, input, select, textarea {margin: 0;}
79 |
80 | /* make buttons play nice in IE */
81 | button {width: auto; overflow: visible;}
82 |
83 | /* scale images in IE7 more attractively */
84 | .ie7 img {-ms-interpolation-mode: bicubic;}
85 |
86 | /* prevent BG image flicker upon hover */
87 | .ie6 html {filter: expression(document.execCommand("BackgroundImageCache", false, true));}
88 |
89 | /* let's clear some floats */
90 | .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
91 | .clearfix:after { clear: both; }
92 | .clearfix { zoom: 1; }
93 |
94 | select, input, textarea, a { outline: none;}
95 |
96 |
97 | input, textarea {
98 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
99 | -moz-box-sizing: border-box; /* Firefox, other Gecko */
100 | box-sizing: border-box; /* Opera/IE 8+ */
101 | }
102 |
--------------------------------------------------------------------------------
/app/controllers/documentation/application_controller.rb:
--------------------------------------------------------------------------------
1 | module Documentation
2 | class ApplicationController < ActionController::Base
3 |
4 | rescue_from Documentation::AccessDeniedError do |e|
5 | render :template => 'documentation/shared/access_denied', :layout => false
6 | end
7 |
8 | rescue_from ActiveRecord::RecordNotFound do |e|
9 | render :template => 'documentation/shared/not_found', :layout => false
10 | end
11 |
12 | before_action do
13 | unless authorizer.can_use_ui?
14 | render :template => 'documentation/shared/not_found', :layout => false
15 | end
16 | end
17 |
18 | private
19 |
20 | def authorizer
21 | @authorizer ||= Documentation.config.authorizer.new(self)
22 | end
23 |
24 | helper_method :authorizer
25 |
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/controllers/documentation/pages_controller.rb:
--------------------------------------------------------------------------------
1 | module Documentation
2 | class PagesController < Documentation::ApplicationController
3 |
4 | before_action :find_page, :only => [:show, :edit, :new, :destroy, :positioning]
5 |
6 | def show
7 | authorizer.check! :view_page, @page
8 | end
9 |
10 | def edit
11 | authorizer.check! :edit_page, @page
12 |
13 | if request.patch?
14 | if @page.update_attributes(safe_params)
15 | redirect_to page_path(@page.full_permalink), :notice => "Page has been saved successfully."
16 | return
17 | end
18 | end
19 | render :action => "form"
20 | end
21 |
22 | def new
23 | authorizer.check! :add_page, @page
24 |
25 | parent = @page
26 | @page = Page.new(:title => "Untitled Page")
27 | if @page.parent = parent
28 | @page.parents = parent.breadcrumb
29 | end
30 |
31 | if request.post?
32 | @page.attributes = safe_params
33 | if @page.save
34 | redirect_to page_path(@page.full_permalink), :notice => "Page created successfully"
35 | return
36 | end
37 | end
38 | render :action => "form"
39 | end
40 |
41 | def destroy
42 | authorizer.check! :delete_page, @page
43 | @page.destroy
44 | redirect_to @page.parent ? page_path(@page.parent.full_permalink) : root_path, :notice => "Page has been removed successfully."
45 | end
46 |
47 | def screenshot
48 | authorizer.check! :upload, @page
49 | if request.post?
50 | @screenshot = Screenshot.new(screenshot_params)
51 | if @screenshot.save
52 | render :json => { :id => @screenshot.id, :title => @screenshot.alt_text, :path => @screenshot.upload.path }, :status => :created
53 | else
54 | render :json => { :errors => @screenshot.errors }, :status => :unprocessible_entity
55 | end
56 | else
57 | @screenshot = Screenshot.new
58 | render 'screenshot', :layout => false
59 | end
60 | end
61 |
62 | def positioning
63 | authorizer.check! :reposition_page, @page
64 | @pages = @page ? @page.children : Page.roots
65 | if request.post?
66 | Page.reorder(@page, params[:order])
67 | render :json => {:status => 'ok'}
68 | end
69 | end
70 |
71 | def search
72 | authorizer.check! :search
73 | @result = Documentation::Page.search(params[:query], :page => params[:page].blank? ? 1 : params[:page].to_i)
74 | end
75 |
76 | private
77 |
78 | def find_page
79 | if params[:path]
80 | @page = Page.find_from_path(params[:path])
81 | end
82 | end
83 |
84 | def safe_params
85 | params.require(:page).permit(:title, :permalink, :content, :favourite)
86 | end
87 |
88 | def screenshot_params
89 | params.require(:screenshot).permit(:upload_file, :alt_text)
90 | end
91 |
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/app/helpers/documentation/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Documentation
2 | module ApplicationHelper
3 |
4 | include Documentation::ViewHelpers
5 |
6 | def flash_messages
7 | flashes = flash.collect do |key,msg|
8 | content_tag :div, content_tag(:p, h(msg)), :id => "flash-#{key}"
9 | end.join.html_safe
10 | end
11 |
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/documentation/page.rb:
--------------------------------------------------------------------------------
1 | module Documentation
2 | class Page < ActiveRecord::Base
3 |
4 | validates :title, :presence => true
5 | validates :position, :presence => true
6 | validates :permalink, :presence => true, :uniqueness => {:scope => :parent_id}
7 |
8 | default_scope -> { order(:position) }
9 | scope :roots, -> { where(:parent_id => nil) }
10 |
11 | parent_options = {:class_name => "Documentation::Page", :foreign_key => 'parent_id'}
12 | parent_options[:optional] = true if ActiveRecord::VERSION::MAJOR >= 5
13 | belongs_to :parent, parent_options
14 |
15 | before_validation do
16 | if self.position.blank?
17 | last_position = self.class.unscoped.where(:parent_id => self.parent_id).order(:position => :desc).first
18 | self.position = last_position ? last_position.position + 1 : 1
19 | end
20 | end
21 |
22 | before_validation :set_permalink
23 | before_save :compile_content
24 |
25 | #
26 | # Ensure the page is updated in the index after saving/destruction
27 | #
28 | after_commit :index, :on => [:create, :update]
29 | after_commit :delete_from_index, :on => :destroy
30 |
31 | #
32 | # Store all the parents of this object. THis is automatically populated when it is loaded
33 | # from a path
34 | #
35 | attr_accessor :parents
36 |
37 | #
38 | # Set the permalink for this page
39 | #
40 | def set_permalink
41 | proposed_permalink = self.title.parameterize
42 | index = 1
43 | while self.permalink.blank?
44 | if self.class.where(:permalink => proposed_permalink, :parent_id => self.parent_id).exists?
45 | index += 1
46 | proposed_permalink = self.title.parameterize + "-#{index}"
47 | else
48 | self.permalink = proposed_permalink
49 | end
50 | end
51 | end
52 |
53 | #
54 | # Return a default empty array for parents
55 | #
56 | def parents
57 | @parents ||= self.parent ? [self.parent.parents, self.parent].flatten : []
58 | end
59 |
60 | #
61 | # Return a full breadcrumb to this page (as it has been loaded)
62 | #
63 | def breadcrumb
64 | @breadcrumb ||= [parents, new_record? ? nil : self].flatten.compact
65 | end
66 |
67 | #
68 | # Return the path where this page can be viewed in the site
69 | #
70 | def preview_path
71 | if path = Documentation.config.preview_path_prefix
72 | "#{path}#{full_permalink}"
73 | else
74 | nil
75 | end
76 | end
77 |
78 | #
79 | # Return a full permalink tot his page
80 | #
81 | def full_permalink
82 | @full_permalink ||= begin
83 | if parents.empty?
84 | self.permalink
85 | else
86 | previous = breadcrumb.compact.map(&:permalink).compact
87 | previous.empty? ? self.permalink : previous.join('/')
88 | end
89 | end
90 | end
91 |
92 | #
93 | # Return all child pages
94 | #
95 | def children
96 | @children ||= begin
97 | if self.new_record?
98 | self.class.none
99 | else
100 | children = self.class.where(:parent_id => self.id)
101 | children.each { |c| c.parents = [parents, self].flatten }
102 | children
103 | end
104 | end
105 | end
106 |
107 | #
108 | # Does this page have children?
109 | #
110 | def has_children?
111 | !children.empty?
112 | end
113 |
114 | #
115 | # Return pages which should be included in the navigation
116 | #
117 | def navigation
118 | if has_children?
119 | root_parent = parents[-1]
120 | if root_parent.nil?
121 | pages = self.class.roots
122 | else
123 | pages = (root_parent || self).children
124 | end
125 | else
126 | root_parent = parents[-2] || parents[-1]
127 | if root_parent.nil? || (root_parent.parent.nil? && parents.size <= 1)
128 | pages = self.class.roots
129 | else
130 | pages = (root_parent || self).children
131 | end
132 | end
133 |
134 |
135 | pages.map do |c|
136 | child_pages = []
137 | child_pages = c.children if breadcrumb.include?(c)
138 | [c, child_pages]
139 | end
140 | end
141 |
142 | #
143 | # Create the compiled content
144 | #
145 | def compile_content
146 | mr = Documentation::MarkdownRenderer.new(:with_toc_data => true)
147 | mr.page = self
148 | rc = Redcarpet::Markdown.new(mr, :space_after_headers => true, :fenced_code_blocks => true, :no_intra_emphasis => true, :highlight => true)
149 | self.compiled_content = rc.render(self.content.to_s).html_safe
150 | end
151 |
152 | #
153 | # Index this page
154 | #
155 | def index
156 | if searcher = Documentation.config.searcher
157 | searcher.index(self)
158 | end
159 | end
160 |
161 | #
162 | # Delete this page from the index
163 | #
164 | def delete_from_index
165 | if searcher = Documentation.config.searcher
166 | searcher.delete(self)
167 | end
168 | end
169 |
170 | #
171 | # Find a page using the searcher if one exists otherwise just fall back to AR
172 | # searching. Returns a Documentation::SearchResult object.
173 | #
174 | def self.search(query, options = {})
175 | if searcher = Documentation.config.searcher
176 | searcher.search(query, options)
177 | end
178 | end
179 |
180 | #
181 | # Find a page by passing a path to the page from the root of the
182 | # site
183 | #
184 | def self.find_from_path(path_string)
185 | raise ActiveRecord::RecordNotFound, "Couldn't find page without a path" if path_string.blank?
186 | path_parts = path_string.split('/')
187 | path = []
188 | path_parts.each_with_index do |p, i|
189 | page = self.where(:parent_id => (path.last ? path.last.id : nil)).find_by_permalink(p)
190 | if page
191 | page.parents = path.dup
192 | page.parent = path.last
193 | path << page
194 | else
195 | raise ActiveRecord::RecordNotFound, "Couldn't find page at #{path_string}"
196 | end
197 | end
198 | path.last
199 | end
200 |
201 | #
202 | # Reorder pgaes
203 | #
204 | def self.reorder(parent, order = [])
205 | order = order.map(&:to_i)
206 | order = self.where(:parent_id => parent.id).map(&:id) if order.empty?
207 | order.each_with_index do |id, index|
208 | command = self.find_by_id!(id)
209 | command.position = index + 1
210 | command.save
211 | end
212 | end
213 |
214 | end
215 | end
216 |
--------------------------------------------------------------------------------
/app/models/documentation/screenshot.rb:
--------------------------------------------------------------------------------
1 | module Documentation
2 | class Screenshot < ActiveRecord::Base
3 |
4 | attachment :upload
5 |
6 | before_validation do
7 | if self.upload_file && self.alt_text.blank?
8 | self.alt_text = self.upload_file.original_filename
9 | end
10 | true
11 | end
12 |
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/views/documentation/pages/_admin_buttons.html.haml:
--------------------------------------------------------------------------------
1 | - canAdd = authorizer.can_add_page?(@page)
2 | - canEdit = authorizer.can_edit_page?(@page)
3 | - canReposition = authorizer.can_reposition_page?(@page)
4 | - canDelete = authorizer.can_delete_page?(@page)
5 |
6 | - if canAdd || canEdit || canReposition || canDelete
7 | %p.adminButtons
8 | - if canAdd
9 | = link_to t('.new'), new_page_path(@page.full_permalink), :class => 'button new'
10 |
11 | - if canEdit
12 | = link_to t('.edit'), edit_page_path(@page.full_permalink), :class => 'button edit'
13 |
14 | - if canReposition
15 | = link_to t('.reorder'), page_positioning_path(@page.parents[-1].try(:full_permalink)), :class => 'button edit'
16 |
17 | - if canDelete
18 | = link_to t('.delete'), delete_page_path(@page.full_permalink), :class => 'button delete', :method => :delete, :data => {:confirm => "Are you sure you wish to remove this page?"}
19 |
--------------------------------------------------------------------------------
/app/views/documentation/pages/form.html.haml:
--------------------------------------------------------------------------------
1 | - @page_title = @page.breadcrumb.reverse.map(&:title).join(" - ")
2 |
3 | %section.content.edit-article
4 | = documentation_breadcrumb_for @page
5 | = form_for @page, :url => @page.new_record? ? new_page_path(@page.parent ? @page.parent.full_permalink : nil) : edit_page_path(@page.full_permalink), :html => {:class => 'pageForm'} do |f|
6 | = error_messages_for @page
7 | %p.title= f.text_field :title, :placeholder => t('.title_placeholder')
8 | %p.content~ f.text_area :content, :placeholder => t('.content_placeholder')
9 | %dl
10 | %dt= f.label :permalink
11 | %dd= f.text_field :permalink, :placeholder => t('.permalink_placeholder')
12 |
13 | %p.submit
14 | - if authorizer.can_upload?(@page)
15 | = link_to t('.add_screenshot'), upload_screenshot_path, :class => 'button preview js-screenshot'
16 | = f.submit t('.save'), :class => 'button'
17 |
--------------------------------------------------------------------------------
/app/views/documentation/pages/index.html.haml:
--------------------------------------------------------------------------------
1 | - @page_title = t('.page_title')
2 | %section.welcome
3 | %h1= t('.welcome_title')
4 | %h2= t('.welcome_text')
5 |
6 | - if Documentation.config.developer_tips
7 | .override
8 | %p= t('.developer_tip').html_safe
9 |
10 | %p.new
11 | - if authorizer.can_add_page?(nil)
12 | = link_to t('.create_root_page'), new_page_path, :class => 'button new'
13 | - if authorizer.can_reposition_page?(nil)
14 | = link_to t('.reorder_root_pages'), page_positioning_path, :class => 'button'
15 |
--------------------------------------------------------------------------------
/app/views/documentation/pages/positioning.html.haml:
--------------------------------------------------------------------------------
1 | - if @page
2 | - @page_title = @page.breadcrumb.reverse.map(&:title).join(" - ")
3 | = documentation_breadcrumb_for @page
4 | - else
5 | - @page_title = t('.reorder_root_pages_title')
6 | = documentation_breadcrumb_for nil
7 |
8 | %section.page
9 | %h1= @page ? @page.title : t('.reorder_root_pages_title')
10 |
11 | = form_tag page_positioning_path(@page.try(:full_permalink)), :class => 'reordering' do
12 | %ul
13 | - for child in @pages
14 | %li
15 | = child.title
16 | = hidden_field_tag 'order[]', child.id
17 |
18 | %p.adminButtons
19 | - if @page
20 | = link_to t('.back_to_page'), page_path(@page.full_permalink), :class => 'button edit'
21 | - else
22 | = link_to t('.back_to_homepage'), root_path, :class => 'button edit'
23 |
--------------------------------------------------------------------------------
/app/views/documentation/pages/screenshot.html.haml:
--------------------------------------------------------------------------------
1 | = form_for @screenshot, :url => upload_screenshot_path, :html => {:class => 'pageForm'} do |f|
2 | %dl
3 | %dt= f.label :upload_file, "Upload File"
4 | %dd.padded
5 | - if params[:filename]
6 | = params[:filename]
7 | - else
8 | = f.file_field :upload_file
9 |
10 | %dl
11 | %dt= f.label :alt_text, "Screenshot Title"
12 | %dd= f.text_field :alt_text
13 |
14 | %p.submit
15 | = f.submit "Upload Screenshot", :class => 'button'
--------------------------------------------------------------------------------
/app/views/documentation/pages/search.html.haml:
--------------------------------------------------------------------------------
1 | - @page_title = t('.title', :query => params[:query])
2 |
3 | %section.page
4 |
5 | - if @result.empty?
6 | %p.noSearchResults= t('.no_results', :query => params[:query])
7 | - else
8 | %h1= t('.title', :query => params[:query])
9 | %p.searchSummary= documentation_search_summary(@result)
10 | = documentation_search_results(@result)
11 | %p.pagination= documentation_search_pagination(@result, :link_class => 'button')
12 |
--------------------------------------------------------------------------------
/app/views/documentation/pages/show.html.haml:
--------------------------------------------------------------------------------
1 | - @page_title = @page.breadcrumb.reverse.map(&:title).join(" - ")
2 |
3 | = documentation_breadcrumb_for @page
4 |
5 | %section.page
6 | - if @page.preview_path
7 | %p.previewLink= link_to t('.view_in_website'), @page.preview_path, :class => 'button preview'
8 |
9 | %h1= @page.title
10 | %p.updated= t('.last_updated', :timestamp => @page.updated_at.to_s(:long))
11 | .documentationMarkdown
12 | ~ preserve(documentation_content_for(@page))
13 |
14 | = render 'admin_buttons'
15 |
--------------------------------------------------------------------------------
/app/views/documentation/shared/access_denied.html.haml:
--------------------------------------------------------------------------------
1 | !!!
2 | %html.errorPage
3 | %head
4 | %title= @page_title
5 | = stylesheet_link_tag 'documentation/application'
6 | = csrf_meta_tags
7 | %body
8 | %section.errorMessage
9 | %h2 Access Denied
10 | %p You are not permitted to view this page. If you believe you have received this message in error, please contact the site admin.
--------------------------------------------------------------------------------
/app/views/documentation/shared/not_found.html.haml:
--------------------------------------------------------------------------------
1 | !!!
2 | %html.errorPage
3 | %head
4 | %title= @page_title
5 | = stylesheet_link_tag 'documentation/application'
6 | = csrf_meta_tags
7 | %body
8 | %section.errorMessage
9 | %h2 Page Not Found
10 | %p No page was found at the path you have entered. Please ensure you have entered the correct URL otherwise contact the site admin.
--------------------------------------------------------------------------------
/app/views/layouts/documentation/_footer.html.haml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamcooke/documentation/e5dfaa64631244fc1a8b8f5bcf5ecc4e0faa103f/app/views/layouts/documentation/_footer.html.haml
--------------------------------------------------------------------------------
/app/views/layouts/documentation/_head.html.haml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamcooke/documentation/e5dfaa64631244fc1a8b8f5bcf5ecc4e0faa103f/app/views/layouts/documentation/_head.html.haml
--------------------------------------------------------------------------------
/app/views/layouts/documentation/_header.html.haml:
--------------------------------------------------------------------------------
1 | %header
2 | %h1= link_to t('.title'), root_path
3 | %p.back= link_to "← #{t('.back_to_site')}".html_safe, "/", :class => ''
4 |
--------------------------------------------------------------------------------
/app/views/layouts/documentation/_search.html.haml:
--------------------------------------------------------------------------------
1 | - if authorizer.can_search?
2 | = form_tag search_path, :method => :get do
3 | %p.searchForm
4 | = text_field_tag 'query', params[:query], :placeholder => t('.query_placeholder')
5 |
--------------------------------------------------------------------------------
/app/views/layouts/documentation/application.html.haml:
--------------------------------------------------------------------------------
1 | !!!
2 | %html
3 | %head
4 | %title #{@page_title} - #{t('.title')}
5 | = stylesheet_link_tag 'documentation/application'
6 | = javascript_include_tag 'documentation/application'
7 | = csrf_meta_tags
8 | = render 'layouts/documentation/head'
9 | = yield :head
10 | %body
11 | = render 'layouts/documentation/header'
12 |
13 | %nav.pages
14 | .inner
15 | = render 'layouts/documentation/search'
16 | = documentation_navigation_tree_for(@page)
17 | %section.content
18 | .inner
19 | = flash_messages
20 | = yield
21 | = render 'layouts/documentation/footer'
22 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | layouts:
3 | documentation:
4 | application:
5 | title: Documentation
6 | header:
7 | title: Documentation
8 | back_to_site: Back to site
9 | search:
10 | query_placeholder: Search Documentation...
11 |
12 | documentation:
13 | helpers:
14 | documentation_breadcrumb_for:
15 | default_root_link: Home
16 | documentation_search_summary:
17 | text: "Found %{total_results} pages matching your query. Showing results %{start_result} to %{end_result}"
18 | documentation_search_results:
19 | in: in
20 | documentation_search_pagination:
21 | next: Next page
22 | previous: Previous page
23 |
24 | pages:
25 | index:
26 | page_title: Welcome
27 | welcome_title: Welcome to Documentation
28 | welcome_text: Choose a page from the menu on the left of the screen to begin.
29 | developer_tip:
30 | You can override this page by adding a new view in app/views/documentation/pages/index
.
31 | Alternatively, you can disable this message by setting developer_tips
to false in the
32 | Documentation configuration.
33 | create_root_page: Create new root page
34 | reorder_root_pages: Re-order root pages
35 |
36 | show:
37 | view_in_website: View in website
38 | last_updated: Last updated on %{timestamp}
39 |
40 | admin_buttons:
41 | new: New page
42 | edit: Edit page
43 | reorder: Re-order children
44 | delete: Delete page
45 |
46 | form:
47 | title_placeholder: Enter a title for this page
48 | content_placeholder: "Use Markdown to enter page content"
49 | permalink_placeholder: Automatically generated if left blank
50 | add_screenshot: Add Screenshot
51 | save: Save Page
52 |
53 | positioning:
54 | reorder_root_pages_title: Re-order root pages
55 | back_to_page: Back to page
56 | back_to_homepage: Back to homepage
57 |
58 | search:
59 | title: "Search results for '%{query}'"
60 | no_results: "No pages were found matching '%{query}'"
61 | next_page: Next page
62 | previous_page: Previous page
63 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Documentation::Engine.routes.draw do
2 |
3 | match 'new(/*path)', :to => 'pages#new', :as => 'new_page', :via => [:get, :post]
4 | match 'positioning(/*path)', :to => 'pages#positioning', :as => 'page_positioning', :via => [:get, :post]
5 | match 'edit(/*path)', :to => 'pages#edit', :as => 'edit_page', :via => [:get, :patch]
6 | match 'delete(/*path)', :to => 'pages#destroy', :as => 'delete_page', :via => [:delete]
7 | match 'screenshot', :to => 'pages#screenshot', :as => 'upload_screenshot', :via => [:get, :post]
8 | get 'search', :to => 'pages#search', :as => 'search'
9 | get '*path' => 'pages#show', :as => 'page'
10 | root :to => 'pages#index'
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20140711185212_create_documentation_pages.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateDocumentationPages < ActiveRecord::Migration[6.0]
4 |
5 | def up
6 | create_table 'documentation_pages' do |t|
7 | t.string :title, :permalink
8 | t.text :content, :compiled_content
9 | t.integer :position
10 | t.belongs_to :parent, polymorphic: true
11 | t.timestamps
12 | end
13 | end
14 |
15 | def down
16 | drop_table :documentation_pages
17 | end
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/db/migrate/20140724111844_create_nifty_attachments_table.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateNiftyAttachmentsTable < ActiveRecord::Migration[6.0]
4 |
5 | def up
6 | create_table :nifty_attachments do |t|
7 | t.belongs_to :parent, polymorphic: true
8 | t.string :parent_type, :token, :digest, :role, :file_name, :file_type
9 | t.binary :data, limit: 10.megabytes
10 | t.timestamps
11 | end
12 | end
13 |
14 | def down
15 | drop_table :nifty_attachments
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/db/migrate/20140724114255_create_documentation_screenshots.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateDocumentationScreenshots < ActiveRecord::Migration[6.0]
4 |
5 | def change
6 | create_table :documentation_screenshots do |t|
7 | t.string :alt_text
8 | end
9 | end
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | $doc_root = File.join(File.expand_path(File.join('..', '..'), __FILE__), 'doc')
2 |
3 | def doc(*path)
4 | File.read(File.join($doc_root, *path))
5 | end
6 |
7 | guide = Documentation::Page.create(:title => "Developers' Guide", :content => doc('developers-guide', 'overview.md'))
8 | guide.children.create(:title => 'Authorization', :content => doc('developers-guide', 'authorization.md'))
9 | guide.children.create(:title => 'Search Backends', :content => doc('developers-guide', 'search-backends.md'))
10 | guide.children.create(:title => 'Customization', :content => doc('developers-guide', 'customization.md'))
11 | views = guide.children.create(:title => 'Building your own views', :content => doc('developers-guide', 'building-views', 'overview.md'))
12 | views.children.create(:title => 'Accessing Pages', :content => doc('developers-guide', 'building-views', 'accessing-pages.md'))
13 | views.children.create(:title => 'Helpers', :content => doc('developers-guide', 'building-views', 'helpers.md'))
14 |
15 | markdown = Documentation::Page.create(:title => "Using Markdown", :content => doc('markdown', 'overview.md'))
--------------------------------------------------------------------------------
/doc/developers-guide/authorization.md:
--------------------------------------------------------------------------------
1 | By default, all pages you create will be visible & editable by anyone. In order to add a level of authorisation, you can define an **Authorizer** what level of access is given to each request.
2 |
3 | ## Creating an authorizer class
4 |
5 | To do this, begin by creating a new class which inherits from `Documentation::Authorizer`.
6 |
7 | ```ruby
8 | class MyDocsAuthorizer < Documentation::Authorizer
9 | end
10 | ```
11 |
12 | Once you have created this class, you can define a number of methods. In all your methods you can access `controller` to access the controller which requested the page. This allows you to access sessions, params or other request information. The methods you can override are as follows:
13 |
14 | * `can_use_ui?` - whether the built-in UI can be used
15 | * `can_search?` - whether searching can take place
16 | * `can_view_page?(page)` - whether the provided page can be viewed
17 | * `can_add_page?(page)` - whether a page can be created within the provided page
18 | * `can_reposition_page?(page)` - whether pages within the provided page can be repositioned
19 | * `can_edit_page?(page)` - whether the provided page can be edited
20 | * `can_delete_page?(page)` - whether the provided page can be deleted
21 | * `can_upload?(page)` - whether a file can be uploaded to this page
22 |
23 | The default for all these method if left un-defined, is `true`. Here's an example:
24 |
25 | ```ruby
26 | def can_edit_page?(page)
27 | controller.request['rack.current_user'].admin?
28 | end
29 | ```
30 |
31 | ## Using your authorizer class
32 |
33 | Once you have created your class, you should tell Documentation that it should use it. Do this by adding the following to your `config/initializers/documentation.rb` file`.
34 |
35 | ```ruby
36 | Documentation.config.authorizer = MyDocsAuthorizer
37 | ```
--------------------------------------------------------------------------------
/doc/developers-guide/building-views/accessing-pages.md:
--------------------------------------------------------------------------------
1 | Everything you need to access the pages stored in the database can be found on the `Documentation::Page` model.
2 |
3 | ## Finding pages
4 |
5 | In most cases, you'll want to find a page based on its path from the root of the site, for example, `example/page/here` may be the string you want to use to find the `here` page.
6 |
7 | ```ruby
8 | page = Documentation::Page.find_from_path('example/page/here')
9 | ```
10 |
11 | ## Useful page methods
12 |
13 | Once you've found a page, there are a number of methods & attribute which may be useful to you.
14 |
15 | * `title` - the page's title
16 | * `content` - the markdown content for a page
17 | * `compiled_content` - the HTML content for a page
18 | * `parents` - returns an array of all the parents for the page
19 | * `breadcrumb` - returns the items which should be included in a breadcrumb trail
20 | * `full_permalink` - the full permalink including all parents
21 | * `children` - all child pages for the page
22 | * `has_children?` - whether or not the page has children
23 | * `navigation` - appropriate navigation pages for this page
24 |
25 | ## Searching
26 |
27 | If you wish to search pages, you should use the `Documentation::Page.search` method as shown below:
28 |
29 | ```ruby
30 | result = Documentation::Page.search('query here', :page => 1)
31 | ```
32 |
33 | Once you have a result object, you can use this to get information about the pages which matched the result. The following methods may be useful:
34 |
35 | * `results` - the pages which have been found
36 | * `excerpt_for(page)` - an excerpt to display for this page
37 | * `empty?` - was the search empty?
38 | * `total_pages` - the total number of pagination pages
39 |
--------------------------------------------------------------------------------
/doc/developers-guide/building-views/helpers.md:
--------------------------------------------------------------------------------
1 | All the helpers needed to do this are provided for you and are shown below. These are the same helpers which are used in the built-in interface.
2 |
3 | -------------
4 |
5 | ### `documentation_breadcrumb_for(page)`
6 |
7 | This helper will return an unordered list containing a breadcrumb trail for the page you provide. The output will look something like this:
8 |
9 | ```html
10 |
17 | ```
18 |
19 | #### Options for this helper
20 |
21 | * `:class` - allows you to set the class for the outer nav element. Defaults to 'breadcrumb'.
22 | * `:root_link` - sets the text for the first link in the breadcrumb. Defaults to 'Home' (taken from the i18n). Set to false to exclude this item.
23 |
24 | -------------
25 |
26 | ### `documentation_navigation_tree_for(page)`
27 |
28 | Provides a single-level nested unordered list which contains the given page. If the given page has no children, its parents will be included. If it has children, they will be shown too. The active page will include the `active` class.
29 |
30 | ```html
31 |
An excerpt goes here
87 |#{title}
" if title 16 | s << Pygments.highlight(code, :lexer => language) 17 | end 18 | rescue 19 | "#{code}
#{text}
" 59 | end 60 | 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/documentation/search_result.rb: -------------------------------------------------------------------------------- 1 | module Documentation 2 | class SearchResult 3 | 4 | attr_accessor :query 5 | attr_accessor :time 6 | attr_accessor :raw_results 7 | attr_accessor :results 8 | attr_accessor :page 9 | attr_accessor :per_page 10 | attr_accessor :total_results 11 | 12 | def initialize 13 | @time = nil 14 | @raw_results = {} 15 | @page = 1 16 | @total_pages = 1 17 | @per_page = nil 18 | end 19 | 20 | # 21 | # Return the pages 22 | # 23 | def results 24 | @results ||= begin 25 | results = Documentation::Page.where(:id => raw_results.keys).includes(:parent).to_a 26 | results.sort_by { |p| raw_results.keys.index(p.id) } 27 | end 28 | end 29 | 30 | # 31 | # Return the highlight string for a given page 32 | # 33 | def excerpt_for(page) 34 | if @raw_results[page.id] && hl = @raw_results[page.id][:highlights] 35 | ERB::Util.html_escape((hl.join("..."))).gsub('{{{', "").gsub("}}}", "").html_safe 36 | else 37 | page.content[0,255].gsub(/[\n\r]/, '') + "..." 38 | end 39 | end 40 | 41 | # 42 | # Is the result set empty? 43 | # 44 | def empty? 45 | self.results.empty? 46 | end 47 | 48 | # 49 | # The total number of pages in the result set 50 | # 51 | def total_pages 52 | (total_results / per_page.to_f).ceil 53 | end 54 | 55 | # 56 | # The number of the first result on the current page 57 | # 58 | def start_result_number 59 | ((page - 1) * per_page) + 1 60 | end 61 | 62 | # 63 | # The number of the last result on the current page 64 | # 65 | def end_result_number 66 | start_result_number + (results.size) - 1 67 | end 68 | 69 | # 70 | # Is this the first page of the result set? 71 | # 72 | def first_page? 73 | page == 1 74 | end 75 | 76 | # 77 | # Is this the last page of the result set? 78 | # 79 | def last_page? 80 | page == total_pages 81 | end 82 | 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/documentation/searchers/abstract.rb: -------------------------------------------------------------------------------- 1 | module Documentation 2 | module Searchers 3 | class Abstract 4 | 5 | attr_reader :options 6 | 7 | def initialize(options = {}) 8 | @options = options 9 | setup 10 | end 11 | 12 | # 13 | # Run whatever initial set up is needed 14 | # 15 | def setup 16 | end 17 | 18 | # 19 | # Search for a page from the index 20 | # 21 | def search(query, options = {}) 22 | [] 23 | end 24 | 25 | # 26 | # Delete a page from the index 27 | # 28 | def delete(page) 29 | false 30 | end 31 | 32 | # 33 | # Reset an index to have no data within it 34 | # 35 | def reset 36 | true 37 | end 38 | 39 | # 40 | # Add or update an page in the index 41 | # 42 | def index(page) 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/documentation/searchers/simple.rb: -------------------------------------------------------------------------------- 1 | module Documentation 2 | module Searchers 3 | class Simple < Documentation::Searchers::Abstract 4 | 5 | def search(query, options = {}) 6 | # Default options 7 | options[:page] ||= 1 8 | options[:per_page] ||= 15 9 | 10 | # Query string 11 | query_string = "content LIKE ? OR title LIKE ?", "%#{query}%", "%#{query}" 12 | 13 | # Get the total number of pages 14 | total_results = pages = Documentation::Page.where(query_string).count 15 | 16 | # Get the actual pages 17 | pages = Documentation::Page.where(query_string) 18 | pages = pages.offset((options[:page].to_i - 1) * options[:per_page].to_i) 19 | pages = pages.limit(options[:per_page].to_i) 20 | 21 | # Create a result object 22 | result = Documentation::SearchResult.new 23 | result.page = options[:page].to_i 24 | result.per_page = options[:per_page].to_i 25 | result.total_results = total_results 26 | result.query = query 27 | result.time = 0 28 | result.results = pages 29 | 30 | # Return the result 31 | result 32 | end 33 | 34 | def index(page) 35 | true 36 | end 37 | 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/documentation/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Documentation 4 | 5 | VERSION = '3.0.0' 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/documentation/view_helpers.rb: -------------------------------------------------------------------------------- 1 | module Documentation 2 | module ViewHelpers 3 | 4 | # 5 | # Path to edit a page in the manager UI 6 | # 7 | def documentation_edit_page_path(page) 8 | "#{::Documentation::Engine.mounted_path}/edit/#{page.full_permalink}" 9 | end 10 | 11 | # 12 | # Path to view a page in the manager UI 13 | # 14 | def documentation_page_path(page) 15 | "#{::Documentation::Engine.mounted_path}/#{page.try(:full_permalink)}" 16 | end 17 | 18 | # 19 | # Return a breadcrumb for the given page 20 | # 21 | def documentation_breadcrumb_for(page, options = {}) 22 | options[:root_link] = options[:root_link].nil? ? t('documentation.helpers.documentation_breadcrumb_for.default_root_link') : options[:root_link] 23 | options[:class] ||= 'breadcrumb' 24 | 25 | String.new.tap do |s| 26 | s << "" 39 | end.html_safe 40 | end 41 | 42 | # 43 | # Return a default navigation tree for the given page 44 | # 45 | def documentation_navigation_tree_for(page, options = {}) 46 | active_page_type = nil 47 | items = String.new.tap do |s| 48 | if page.is_a?(::Documentation::Page) 49 | 50 | pages = page.navigation.select { |p,c| documentation_authorizer.can_view_page?(p) } 51 | 52 | pages.each do |p, children| 53 | s << "{{nav}}
") do 102 | children = page.children 103 | children = children.select { |c| documentation_authorizer.can_view_page?(c) } 104 | items = children.map { |c| "You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |