amber 0.2.6

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.
@@ -0,0 +1,66 @@
1
+ #
2
+ # A Layout is similar to a layout in Rails (a template that decorates the pages)
3
+ #
4
+
5
+ gem 'tilt', '>= 2.0.0'
6
+ require 'tilt'
7
+ require 'haml'
8
+
9
+ #Haml::Options.defaults[:format] = :html5
10
+
11
+ module Amber
12
+ module Render
13
+
14
+ class Layout
15
+ def self.load(layout_dir=nil)
16
+ @layout_dirs ||= []
17
+ @layout_dirs << layout_dir if layout_dir
18
+ reload
19
+ end
20
+
21
+ def self.reload
22
+ @layouts ||= {}
23
+ @layouts['default'] = DefaultLayout.new
24
+ @layout_dirs.each do |dir|
25
+ Dir.glob("#{dir}/*").each do |layout_file|
26
+ name = File.basename(layout_file).sub(/^([^\.]*).*$/, "\\1")
27
+ @layouts[name] = Layout.new(layout_file)
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.[](layout)
33
+ @layouts[layout]
34
+ end
35
+
36
+ def initialize(file_path, &block)
37
+ if file_path =~ /\.haml$/
38
+ @template = Tilt::HamlTemplate.new(file_path, {:format => :html5})
39
+ else
40
+ @template = Tilt.new(file_path, &block)
41
+ end
42
+ end
43
+
44
+ def render(view, &block)
45
+ @template.render(view, &block)
46
+ end
47
+ end
48
+
49
+ class DefaultLayout < Layout
50
+ def initialize
51
+ @template = Tilt::StringTemplate.new {DEFAULT}
52
+ end
53
+ DEFAULT = '<!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <title>#{ @page.nav_title } - #{@site.title}</title>
57
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
58
+ </head>
59
+ <body>
60
+ #{ yield }
61
+ </body>
62
+ </html>'
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,383 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nokogiri'
4
+ require 'cgi'
5
+
6
+ #
7
+ # Generates a table of contents for any HTML markup, and adds anchors to headings.
8
+ #
9
+
10
+ module Amber::Render
11
+
12
+ ##
13
+ ## TABLE OF CONTENTS
14
+ ##
15
+
16
+ class TableOfContents
17
+ #
18
+ # options:
19
+ # :content_selector (css selector for headings, nokogiri backend only)
20
+ # :href_base -- use this href for the toc links
21
+ # :numeric_prefix -- prefix toc entries and headings with numeric counter (e.g. 1.1.0, 1.2.0, ...)
22
+ #
23
+ def initialize(html, options = {})
24
+ @html = html
25
+ @toc = TocItem.new
26
+ @levels = {"h1" => 0, "h2" => 0, "h3" => 0, "h4" => 0}
27
+ @heading_anchors = {}
28
+ @options = options
29
+ @options[:tag] ||= 'ol'
30
+ end
31
+
32
+ def to_html
33
+ parse_doc unless @parsed
34
+ # override this!
35
+ end
36
+
37
+ def to_toc
38
+ parse_doc unless @parsed
39
+ # override this!
40
+ end
41
+
42
+ private
43
+
44
+ def parse_doc
45
+ each_heading(@html) do |heading, heading_text|
46
+ heading_anchor = anchor_text(heading_text)
47
+ heading_text = strip_anchors(heading_text)
48
+ if @options[:numeric_prefix]
49
+ increment_level(heading)
50
+ heading_text = level_text + " " + heading_text
51
+ end
52
+ @toc.add_heading(heading, heading_text, heading_anchor)
53
+ '<a name="%s"></a>%s' % [heading_anchor, heading_text]
54
+ end
55
+ @parsed = true
56
+ end
57
+
58
+ #
59
+ # returns anchor text from heading text.
60
+ # e.g. First Heading! => first-heading
61
+ #
62
+ # if there are duplicates, they get numbered:
63
+ # heading => heading
64
+ # heading => heading-2
65
+ # heading => heading-3
66
+ #
67
+ def anchor_text(heading_text)
68
+ text = nameize(strip_html_tags(heading_text))
69
+ text_with_suffix = text
70
+ i = 2
71
+ while @heading_anchors[text_with_suffix]
72
+ text_with_suffix = "#{text}-#{i}"
73
+ i+=1
74
+ end
75
+ @heading_anchors[text_with_suffix] = true
76
+ text_with_suffix
77
+ end
78
+
79
+ #
80
+ # convert any string to one suitable for a url.
81
+ # resist the urge to translit non-ascii slugs to ascii.
82
+ # it is always much better to keep strings as utf8.
83
+ #
84
+ def nameize(str)
85
+ str = str.dup
86
+ str.gsub!(/&(\w{2,6}?|#[0-9A-Fa-f]{2,6});/,'') # remove html entitities
87
+ str.gsub!(/[^- [[:word:]]]/u, '') # remove non-word characters (using unicode definition of a word char)
88
+ str.strip!
89
+ str.downcase! # upper case characters in urls are confusing
90
+ str.gsub!(/\ +/u, '-') # spaces to dashes, preferred separator char everywhere
91
+ CGI.escape(str)
92
+ end
93
+
94
+ # removes all html markup
95
+ def strip_html_tags(html)
96
+ Nokogiri::HTML::DocumentFragment.parse(html, 'UTF-8').children.collect{|child| child.inner_text}.join
97
+ end
98
+
99
+ # remove <a name='x'></a> from html, but leaves all other tags in place.
100
+ def strip_anchors(html)
101
+ Nokogiri::HTML::DocumentFragment.parse(html, 'UTF-8').children.collect{|child|
102
+ if child.name == "text"
103
+ child.inner_text
104
+ elsif child.name != 'a' || !child.attributes.detect{|atr| atr[0] == 'name'}
105
+ child.to_s
106
+ end
107
+ }.join
108
+ end
109
+
110
+ #
111
+ # prefix headings with text like 1.2.1, if :numeric_prefix => true
112
+ #
113
+ def level_text
114
+ [@levels["h1"], @levels["h2"], @levels["h3"], @levels["h4"]].join(".").gsub(/\.0/, "")
115
+ end
116
+
117
+ #
118
+ # keeps a counter of the latest heading at each level
119
+ #
120
+ def increment_level(heading)
121
+ @levels[heading] += 1
122
+ @levels["h2"] = 0 if heading == "h1"
123
+ @levels["h3"] = 0 if heading == "h1" || heading == "h2"
124
+ @levels["h4"] = 0 if heading == "h1" || heading == "h2" || heading == "h3"
125
+ end
126
+
127
+ def each_heading(html, &block)
128
+ raise 'override me'
129
+ end
130
+ end
131
+
132
+ ##
133
+ ## NOKOGIRI TOC
134
+ ##
135
+
136
+ class NokogiriTableOfContents < TableOfContents
137
+ def to_html
138
+ super
139
+ @nokogiri_doc.to_html.gsub(/(<h\d.*?>)\n/, '\1').gsub(/\n(<\/h\d.*?>)/, '\1')
140
+ end
141
+
142
+ def to_toc
143
+ super
144
+ ul = Nokogiri::XML::Node.new(@options[:tag], Nokogiri::HTML.fragment(""))
145
+ @toc.populate_node(ul, @options)
146
+ ul.to_pretty_html
147
+ end
148
+
149
+ private
150
+
151
+ def each_heading(html, &block)
152
+ @nokogiri_doc = Nokogiri::HTML.fragment(html, "UTF-8")
153
+ if @options[:content_selector]
154
+ selector = @levels.keys.map {|h| "#{@options[:content_selector]} #{h}" }.join(",")
155
+ else
156
+ selector = @levels.keys.join(",")
157
+ end
158
+ @nokogiri_doc.css(selector).each do |node|
159
+ node.inner_html = yield(node.name, node.inner_html)
160
+ end
161
+ end
162
+ end
163
+
164
+ ##
165
+ ## REGEX TOC
166
+ ##
167
+
168
+ class RegexTableOfContents < TableOfContents
169
+ def to_html
170
+ super
171
+ @new_html
172
+ end
173
+
174
+ def to_toc
175
+ super
176
+ @toc.to_html(@options)
177
+ end
178
+
179
+ private
180
+
181
+ HEADING_EX = %r{
182
+ <\s*((h\d).*?)\s*> # match starting <h1>
183
+ (.+)? # match innner text
184
+ <\s*\/\2\s*> # match closing </h1>
185
+ }x
186
+
187
+ def each_heading(html, &block)
188
+ @new_html = html.gsub(HEADING_EX) do |match|
189
+ "<%s>%s</%s>" % [$1, yield($2, $3), $2]
190
+ end
191
+ end
192
+ end
193
+
194
+ ##
195
+ ## TOC ITEM
196
+ ##
197
+ ## A tree of TocItems composes the table of contents outline.
198
+ ##
199
+
200
+ class TocItem
201
+ attr_reader :children, :level, :text, :anchor
202
+
203
+ def initialize(heading='h0', text=nil, anchor=nil)
204
+ @level = heading[1].to_i if heading.is_a?(String)
205
+ @text = text
206
+ @anchor = anchor
207
+ @children = []
208
+ end
209
+
210
+ def add_heading(heading, heading_text, heading_anchor)
211
+ self.parent_for(heading).children << TocItem.new(heading, heading_text, heading_anchor)
212
+ end
213
+
214
+ #
215
+ # generates nokogiri html node tree from this toc
216
+ #
217
+ def populate_node(node, options)
218
+ @children.each do |item|
219
+ li = node.document.create_element("li")
220
+ li.add_child(li.document.create_element("a", item.text, :href => "#{options[:href_base]}##{item.anchor}"))
221
+ if item.children.any?
222
+ ul = li.document.create_element(options[:tag])
223
+ item.populate_node(ul, options)
224
+ li.add_child(ul)
225
+ end
226
+ node.add_child(li)
227
+ end
228
+ end
229
+
230
+ #
231
+ # generates html string from this toc
232
+ #
233
+ def to_html(options={})
234
+ html = []
235
+ tag = options[:tag]
236
+ indent = options[:indent] || 0
237
+ str = options[:indent_str] || " "
238
+ html << '%s<%s>' % [(str*indent), tag]
239
+ @children.each do |item|
240
+ html << '%s<li>' % (str*(indent+1))
241
+ html << '%s<a href="%s#%s">%s</a>' % [str*(indent+2), options[:href_base], item.anchor, item.text]
242
+ if item.children.any?
243
+ html << item.to_html({
244
+ :indent => indent+2,
245
+ :indent_str => str,
246
+ :tag => tag,
247
+ :href_base => options[:href_base]
248
+ })
249
+ end
250
+ html << '%s</li>' % (str*(indent+1))
251
+ end
252
+ html << '%s</%s>' % [(str*indent), tag]
253
+ html.join("\n")
254
+ end
255
+
256
+ #
257
+ # Returns the appropriate TocItem for appending a new item
258
+ # at a particular heading level.
259
+ #
260
+ def parent_for(heading)
261
+ heading = heading[1].to_i if heading.is_a?(String)
262
+ if children.any? && children.last.level < heading
263
+ children.last.parent_for(heading)
264
+ else
265
+ self
266
+ end
267
+ end
268
+ end
269
+
270
+ end
271
+
272
+ class Nokogiri::XML::Node
273
+ def to_pretty_html(indent=0)
274
+ indent_str = " " * indent
275
+ children_html = []
276
+ text_html = nil
277
+ if children.size == 1 && children.first.name == "text"
278
+ text_html = children.first.content
279
+ else
280
+ children.each do |child|
281
+ if child.name == "text"
282
+ children_html << "#{" " * (indent+1)}#{child.content}" if !child.content.empty?
283
+ else
284
+ children_html << child.to_pretty_html(indent+1)
285
+ end
286
+ end
287
+ end
288
+ attrs = []
289
+ attributes.each do |attribute|
290
+ attrs << %(#{attribute[0]}="#{attribute[1]}")
291
+ end
292
+ if attrs.any?
293
+ attr_html = " " + attrs.join(' ')
294
+ else
295
+ attr_html = ""
296
+ end
297
+ html = []
298
+ if text_html
299
+ html << "#{indent_str}<#{name}#{attr_html}>#{text_html}</#{name}>"
300
+ elsif children_html.any?
301
+ html << "#{indent_str}<#{name}#{attr_html}>"
302
+ html += children_html
303
+ html << "#{indent_str}</#{name}>"
304
+ else
305
+ html << "#{indent_str}<#{name}#{attr_html}></#{name}>"
306
+ end
307
+ html.join("\n")
308
+ end
309
+ end
310
+
311
+
312
+ =begin
313
+
314
+ AN ATTEMPT TO GET NOKOGIRI TO OUTPUT REASONABLE HTML5. NO LUCK.
315
+
316
+ #
317
+ # convert a Nokogiri::HTML::Document into well formatted html.
318
+ # unfortunately, Nokogiri formatting only works on complete documents, so we strip away the <html> tags. :(
319
+ #
320
+ def format_doc(doc)
321
+ INDENT_XSLT.apply_to(doc).to_s.sub("<!DOCTYPE html>\n<html><body>", '').sub('</body></html>', '')
322
+ end
323
+
324
+ # from https://github.com/jarijokinen/html5-beautifier/blob/master/lib/html5-beautifier/xslt/html5-beautifier.xslt
325
+ # MIT License
326
+ INDENT_XSLT_STRING = <<EOF
327
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
328
+ <xsl:output method="html" omit-xml-declaration="yes" encoding="utf-8" />
329
+ <xsl:param name="indent-increment" select="'__INDENT_STRING__'" />
330
+
331
+ <xsl:template name="newline">
332
+ <xsl:text disable-output-escaping="yes">
333
+ </xsl:text>
334
+ </xsl:template>
335
+
336
+ <xsl:template match="/">
337
+ <xsl:text disable-output-escaping="yes">&lt;!DOCTYPE html></xsl:text>
338
+ <xsl:apply-templates />
339
+ </xsl:template>
340
+
341
+ <xsl:template match="comment() | processing-instruction()">
342
+ <xsl:param name="indent" select="''" />
343
+ <xsl:call-template name="newline" />
344
+ <xsl:value-of select="$indent" />
345
+ <xsl:copy />
346
+ </xsl:template>
347
+
348
+ <xsl:template match="text()">
349
+ <xsl:param name="indent" select="''" />
350
+ <xsl:call-template name="newline" />
351
+ <xsl:value-of select="$indent" />
352
+ <xsl:value-of select="normalize-space(.)" />
353
+ </xsl:template>
354
+
355
+ <xsl:template match="text()[normalize-space(.)='']" />
356
+
357
+ <xsl:template match="*">
358
+ <xsl:param name="indent" select="''" />
359
+ <xsl:call-template name="newline" />
360
+ <xsl:value-of select="$indent" />
361
+ <xsl:choose>
362
+ <xsl:when test="count(child::*) > 0 and __EXCLUDE_ELEMENTS__">
363
+ <xsl:copy>
364
+ <xsl:copy-of select="@*" />
365
+ <xsl:apply-templates select="*|text()">
366
+ <xsl:with-param name="indent" select="concat($indent, $indent-increment)" />
367
+ </xsl:apply-templates>
368
+ <xsl:call-template name="newline" />
369
+ <xsl:value-of select="$indent" />
370
+ </xsl:copy>
371
+ </xsl:when>
372
+ <xsl:otherwise>
373
+ <xsl:copy-of select="." />
374
+ </xsl:otherwise>
375
+ </xsl:choose>
376
+ </xsl:template>
377
+ </xsl:stylesheet>
378
+ EOF
379
+
380
+ INDENT_XSLT = Nokogiri::XSLT(INDENT_XSLT_STRING.gsub('__INDENT_STRING__', ' '))
381
+
382
+
383
+ =end
@@ -0,0 +1,158 @@
1
+ require 'haml'
2
+ require 'tilt'
3
+ require 'RedCloth'
4
+ require 'rdiscount'
5
+
6
+ module Amber
7
+ module Render
8
+ class Template
9
+
10
+ PROPERTY_HEADER = /^\s*(^(|- )@\w[^\n]*?\n)*/m
11
+
12
+ RENDER_MAP = {
13
+ :text => 'render_textile',
14
+ :textile => 'render_textile',
15
+ :md => 'render_markdown',
16
+ :markdown => 'render_markdown',
17
+ :html => 'render_raw'
18
+ }
19
+
20
+ TEXTILE_TOC_RE = /^\s*h([1-6])\.\s+(.*)/
21
+
22
+ ERB_TAG_RE = /<%=.*?%>/
23
+ ERB_PLACEHOLDER_RE = /xx erb tag\d+ xx/
24
+
25
+ attr_reader :file
26
+ attr_reader :type
27
+ attr_reader :content
28
+ attr_reader :partial
29
+
30
+ def initialize(options={})
31
+ if options[:file]
32
+ @file = options[:file]
33
+ @type = type_from_file(@file)
34
+ elsif options[:content]
35
+ @content = options[:content]
36
+ @type = options[:type] # e.g. :haml. required if @content
37
+ end
38
+ @partial = options[:partial]
39
+ end
40
+
41
+ #
42
+ # returns rendered content or title, depending on render_mode
43
+ #
44
+ def render(view, options={})
45
+ view.locals[:_type] = @type
46
+ render_mode = options.delete(:mode) || :content
47
+ toc = options.delete(:toc)
48
+
49
+ if render_mode == :title
50
+ render_title(view)
51
+ else
52
+ html = render_html(view)
53
+ if render_mode == :toc
54
+ RegexTableOfContents.new(html, options).to_toc
55
+ elsif toc || render_mode == :toc_and_content
56
+ toc = RegexTableOfContents.new(html, options)
57
+ %(<div id="TOC">%s</div>\n\n%s) % [toc.to_toc, toc.to_html]
58
+ else
59
+ html
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def render_html(view)
67
+ if @type == :haml
68
+ return render_haml(@file, view)
69
+ else
70
+ @content ||= File.read(@file, :encoding => 'UTF-8').sub(PROPERTY_HEADER, '') # remove property header
71
+ if method = RENDER_MAP[@type]
72
+ content, erb_tags = replace_erb_tags(@content)
73
+ html = self.send(method, view, content)
74
+ return render_erb(restore_erb_tags(html, erb_tags), view)
75
+ else
76
+ return "sorry, i don't understand how to render `#{@type}`"
77
+ end
78
+ end
79
+ end
80
+
81
+ def render_erb(string, view)
82
+ template = Tilt::ERBTemplate.new {string}
83
+ template.render(view)
84
+ end
85
+
86
+ #
87
+ # takes raw markup, and replaces every <%= x %> with a
88
+ # markup-safe placeholder. erb_tags holds a map of placeholder
89
+ # to original erb. e.g. {"ERBTAG0" => "<%= 'hi]]"}
90
+ #
91
+ def replace_erb_tags(content)
92
+ counter = 0
93
+ erb_tags = {}
94
+ new_content = content.gsub(ERB_TAG_RE) do |match|
95
+ placeholder = "xx erb tag#{counter} xx"
96
+ erb_tags[placeholder] = match
97
+ counter+=1
98
+ placeholder
99
+ end
100
+ return [new_content, erb_tags]
101
+ end
102
+
103
+ #
104
+ # replaces erb placeholders with actual erb
105
+ #
106
+ def restore_erb_tags(html, erb_tags)
107
+ html.gsub(ERB_PLACEHOLDER_RE) do |match|
108
+ erb_tags[match]
109
+ end
110
+ end
111
+
112
+ def render_title(view)
113
+ locale = view.locals[:locale]
114
+ if title = view.page.explicit_title(locale)
115
+ "<h1>#{title}</h1>\n"
116
+ else
117
+ ""
118
+ end
119
+ end
120
+
121
+ def render_haml(file_path, view)
122
+ template = Tilt::HamlTemplate.new(file_path, {:format => :html5, :default_encoding => 'UTF-8'})
123
+ add_bracket_links(view, template.render(view))
124
+ end
125
+
126
+ def render_textile(view, content)
127
+ content = add_bracket_links(view, content)
128
+ Autolink.auto_link(RedCloth.new(content).to_html)
129
+ end
130
+
131
+ def render_markdown(view, content)
132
+ content = add_bracket_links(view, content)
133
+ RDiscount.new(content, :smart, :autolink).to_html
134
+ end
135
+
136
+ def render_raw(view, content)
137
+ add_bracket_links(view, content)
138
+ end
139
+
140
+ def add_bracket_links(view, content)
141
+ content = Bracketlink.bracket_link(content) do |from, to|
142
+ view.link({from => to})
143
+ end
144
+ content
145
+ end
146
+
147
+ def type_from_file(file_path)
148
+ suffix = File.extname(file_path)
149
+ if suffix
150
+ suffix.sub! /^\./, ''
151
+ suffix = suffix.to_sym
152
+ end
153
+ suffix
154
+ end
155
+
156
+ end
157
+ end
158
+ end