asciidoctor-bespoke 1.0.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.adoc +22 -0
- data/README.adoc +807 -0
- data/Rakefile +7 -0
- data/bin/asciidoctor-bespoke +20 -0
- data/lib/asciidoctor-bespoke.rb +2 -0
- data/lib/asciidoctor-bespoke/converter.rb +48 -0
- data/lib/asciidoctor-bespoke/version.rb +5 -0
- data/templates/slim/admonition.html.slim +4 -0
- data/templates/slim/audio.html.slim +3 -0
- data/templates/slim/document.html.slim +69 -0
- data/templates/slim/helpers.rb +222 -0
- data/templates/slim/image.html.slim +24 -0
- data/templates/slim/listing.html.slim +15 -0
- data/templates/slim/literal.html.slim +10 -0
- data/templates/slim/notes.html.slim +2 -0
- data/templates/slim/olist.html.slim +14 -0
- data/templates/slim/open.html.slim +4 -0
- data/templates/slim/paragraph.html.slim +9 -0
- data/templates/slim/quote.html.slim +14 -0
- data/templates/slim/section.html.slim +32 -0
- data/templates/slim/sidebar.html.slim +7 -0
- data/templates/slim/slide_speaker.html.slim +13 -0
- data/templates/slim/slide_title.html.slim +54 -0
- data/templates/slim/ulist.html.slim +14 -0
- data/templates/slim/video.html.slim +36 -0
- metadata +129 -0
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
if File.file?(asciidoctor_bespoke = (File.expand_path '../../lib/asciidoctor-bespoke.rb', __FILE__))
|
4
|
+
require asciidoctor_bespoke
|
5
|
+
else
|
6
|
+
require 'asciidoctor-bespoke'
|
7
|
+
end
|
8
|
+
require 'asciidoctor/cli'
|
9
|
+
|
10
|
+
options = Asciidoctor::Cli::Options.new backend: 'bespoke'
|
11
|
+
|
12
|
+
# FIXME This is a really bizarre API. Please make me simpler.
|
13
|
+
if Integer === (result = options.parse! ARGV)
|
14
|
+
exit result
|
15
|
+
else
|
16
|
+
invoker = Asciidoctor::Cli::Invoker.new options
|
17
|
+
GC.start
|
18
|
+
invoker.invoke!
|
19
|
+
exit invoker.code
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'asciidoctor/converter/html5'
|
2
|
+
require 'asciidoctor/converter/composite'
|
3
|
+
require 'asciidoctor/converter/template'
|
4
|
+
|
5
|
+
# Asciidoctor < 1.5.5 doesn't recognize svg tag name if followed immediately by newline
|
6
|
+
Asciidoctor::Converter::Html5Converter.tap do |klass|
|
7
|
+
klass.send :remove_const, :SvgPreambleRx
|
8
|
+
klass.const_set :SvgPreambleRx, /\A.*?(?=<svg\b)/m
|
9
|
+
end if Asciidoctor::VERSION == '1.5.3' || Asciidoctor::VERSION == '1.5.4'
|
10
|
+
|
11
|
+
module Asciidoctor
|
12
|
+
module Bespoke
|
13
|
+
class Converter < ::Asciidoctor::Converter::CompositeConverter
|
14
|
+
ProvidedTemplatesDir = ::File.expand_path '../../../templates', __FILE__
|
15
|
+
SlimPrettyOpts = { pretty: true, indent: false }.freeze
|
16
|
+
register_for 'bespoke'
|
17
|
+
|
18
|
+
def initialize backend, opts = {}
|
19
|
+
template_dirs = [ProvidedTemplatesDir] # last dir wins
|
20
|
+
if (custom_template_dirs = opts[:template_dirs])
|
21
|
+
template_dirs += custom_template_dirs.map {|d| ::File.expand_path d }
|
22
|
+
end
|
23
|
+
include_dirs = template_dirs.reverse.tap {|c| c << (::File.join c.pop, 'slim') } # first dir wins
|
24
|
+
engine_opts = (opts[:template_engine_options] || {}).dup
|
25
|
+
extra_slim_opts = { include_dirs: include_dirs }
|
26
|
+
extra_slim_opts.update SlimPrettyOpts if Set.new(%w(1 true)).include?(ENV['SLIM_PRETTY'].to_s)
|
27
|
+
engine_opts[:slim] = (engine_opts.key? :slim) ? (extra_slim_opts.merge engine_opts[:slim]) : extra_slim_opts
|
28
|
+
template_opts = opts.merge htmlsyntax: 'html', template_engine: 'slim', template_engine_options: engine_opts
|
29
|
+
template_converter = ::Asciidoctor::Converter::TemplateConverter.new backend, template_dirs, template_opts
|
30
|
+
html5_converter = ::Asciidoctor::Converter::Html5Converter.new backend, opts
|
31
|
+
super backend, template_converter, html5_converter
|
32
|
+
basebackend 'html'
|
33
|
+
htmlsyntax 'html'
|
34
|
+
end
|
35
|
+
|
36
|
+
def convert node, transform = nil, opts = {}
|
37
|
+
if (node.attributes.delete 'skip-option') || node.context == :preamble
|
38
|
+
''
|
39
|
+
# FIXME mixin slide? method to AbstractBlock (or AbstractNode)
|
40
|
+
elsif node.context == :section && node.level == 1 && (node.attr? 'transform')
|
41
|
+
super node, %(slide_#{node.attr 'transform'}), opts
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
doctype 5
|
2
|
+
html lang=(attr :lang, 'en' unless attr? :nolang)
|
3
|
+
head
|
4
|
+
meta charset=(attr :encoding, 'utf-8')
|
5
|
+
/ QUESTION should we add maximum-scale=1 to viewport setting?
|
6
|
+
meta name='viewport' content='width=device-width, initial-scale=1'
|
7
|
+
title=(doctitle sanitize: true, use_fallback: true).tr_s(' ', ' ')
|
8
|
+
- [:description, :keywords, :author, :copyright].each do |_key|
|
9
|
+
- if attr? _key
|
10
|
+
meta name=_key content=(attr _key)
|
11
|
+
meta name='generator' content="Asciidoctor #{attr 'asciidoctor-version'} (Bespoke.js converter)"
|
12
|
+
- if attr? :favicon
|
13
|
+
- if (_favicon_href = attr :favicon).empty?
|
14
|
+
- _favicon_href = 'favicon.ico'
|
15
|
+
- if (_favicon_image_type = File.extname(_favicon_href)[1..-1]) == 'ico'
|
16
|
+
- _favicon_image_type = 'x-icon'
|
17
|
+
link rel='shortcut icon' href=_favicon_href type="image/#{_favicon_image_type}"
|
18
|
+
meta name='mobile-web-app-capable' content='yes'
|
19
|
+
- if attr? :appname
|
20
|
+
meta name='application-name' content=(attr :appname)
|
21
|
+
link rel='stylesheet' href='build/build.css'
|
22
|
+
- if attr? :icons, 'font'
|
23
|
+
- if attr? 'iconfont-remote'
|
24
|
+
link rel='stylesheet' href=(attr 'iconfont-cdn', (cdn_uri 'font-awesome', '4.5.0', 'css/font-awesome.min.css'))
|
25
|
+
- else
|
26
|
+
link rel='stylesheet' href=(normalize_web_path 'font-awesome.css', (attr :stylesdir, ''))
|
27
|
+
- case (_source_highlighter = attr 'source-highlighter')
|
28
|
+
- when 'highlight.js'
|
29
|
+
link rel='stylesheet' href="#{_highlightjs_path = attr 'highlightjsdir', (cdn_uri 'highlight.js', '9.1.0')}/styles/#{attr 'highlightjs-theme', 'github'}.min.css"
|
30
|
+
- when 'prettify'
|
31
|
+
link rel='stylesheet' href="#{_prettify_path = attr 'prettifydir', (cdn_uri 'prettify', 'r298')}/#{attr 'prettify-theme', 'prettify'}.min.css"
|
32
|
+
- unless (_docinfo = docinfo :head, '-bespoke.html').empty?
|
33
|
+
=_docinfo
|
34
|
+
body id=id
|
35
|
+
article.deck
|
36
|
+
- unless noheader
|
37
|
+
/ TODO enable :header docinfo once Asciidoctor 1.5.5 is available
|
38
|
+
/- unless (_docinfo = docinfo :header, '-bespoke.html').empty?
|
39
|
+
/ =_docinfo
|
40
|
+
- if header?
|
41
|
+
include slide_title.html.slim
|
42
|
+
=content
|
43
|
+
- unless (_docinfo = docinfo :footer, '-bespoke.html').empty?
|
44
|
+
=_docinfo
|
45
|
+
script src='build/build.js'
|
46
|
+
- case _source_highlighter
|
47
|
+
- when 'highlight.js'
|
48
|
+
script src="#{_highlightjs_path}/highlight.min.js"
|
49
|
+
|<script>Array.prototype.forEach.call(document.querySelectorAll('i.conum+b'),function(n){n.parentNode.removeChild(n)});</script>
|
50
|
+
|<script>hljs.initHighlighting()</script>
|
51
|
+
- when 'prettify'
|
52
|
+
script src="#{_prettify_path}/prettify.js"
|
53
|
+
|<script>Array.prototype.forEach.call(document.querySelectorAll('i.conum+b'),function(n){n.parentNode.removeChild(n)})</script>
|
54
|
+
|<script>prettyPrint()</script>
|
55
|
+
- if attr? :stem
|
56
|
+
script type='text/x-mathjax-config'
|
57
|
+
|MathJax.Hub.Config({
|
58
|
+
messageStyle: "none",
|
59
|
+
tex2jax: {
|
60
|
+
inlineMath: [#{Asciidoctor::INLINE_MATH_DELIMITERS[:latexmath].inspect}],
|
61
|
+
displayMath: [#{Asciidoctor::BLOCK_MATH_DELIMITERS[:latexmath].inspect}],
|
62
|
+
ignoreClass: "nostem|nolatexmath"
|
63
|
+
},
|
64
|
+
asciimath2jax: {
|
65
|
+
delimiters: [#{Asciidoctor::BLOCK_MATH_DELIMITERS[:asciimath].inspect}],
|
66
|
+
ignoreClass: "nostem|noasciimath"
|
67
|
+
}
|
68
|
+
})
|
69
|
+
script src="#{cdn_uri 'mathjax', '2.6.0', 'MathJax.js?config=TeX-MML-AM_HTMLorMML'}"
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# This module gets mixed in to every node (the context of the template) at the
|
2
|
+
# time the node is being converted. The properties and methods in this module
|
3
|
+
# effectively become direct members of the template.
|
4
|
+
module Slim::Helpers
|
5
|
+
CDN_BASE = '//cdnjs.cloudflare.com/ajax/libs'
|
6
|
+
EOL = %(\n)
|
7
|
+
BUILD_ROLE_BY_TYPE = {
|
8
|
+
'self' => 'build',
|
9
|
+
'items' => 'build-items'
|
10
|
+
}
|
11
|
+
|
12
|
+
SvgStartTagRx = /\A<svg[^>]*>/
|
13
|
+
ViewBoxAttributeRx = /\sview[bB]ox="[^"]+"/
|
14
|
+
WidthAttributeRx = /\swidth="([^"]+)"/
|
15
|
+
HeightAttributeRx = /\sheight="([^"]+)"/
|
16
|
+
SliceHintRx = / +/
|
17
|
+
|
18
|
+
# Capture nested template content and register it with the specified key, to
|
19
|
+
# be executed at a later time.
|
20
|
+
#
|
21
|
+
# This method must be invoked using the control code directive (i.e., -). By
|
22
|
+
# using a control code directive, the block is set up to append the result
|
23
|
+
# directly to the output buffer. (Integrations often hide the distinction
|
24
|
+
# between a control code directive and an output directive in this context).
|
25
|
+
#
|
26
|
+
# key - The Symbol under which to save the template block.
|
27
|
+
# opts - A Hash of options to control processing (default: {}):
|
28
|
+
# * :append - A Boolean that indicates whether to append this block
|
29
|
+
# to others registered with this key (default: false).
|
30
|
+
# * :content - String content to be used if template content is not
|
31
|
+
# provided (optional).
|
32
|
+
# block - The template content (in Slim template syntax).
|
33
|
+
#
|
34
|
+
# Examples
|
35
|
+
#
|
36
|
+
# - content_for :body
|
37
|
+
# p content
|
38
|
+
# - content_for :body, append: true
|
39
|
+
# p more content
|
40
|
+
#
|
41
|
+
# Returns nothing.
|
42
|
+
def content_for key, opts = {}, &block
|
43
|
+
@content = {} unless defined? @content
|
44
|
+
(opts[:append] ? (@content[key] ||= []) : (@content[key] = [])) << (block_given? ? block : lambda { opts[:content] })
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Checks whether deferred template content has been registered for the specified key.
|
49
|
+
#
|
50
|
+
# key - The Symbol under which to look for saved template blocks.
|
51
|
+
#
|
52
|
+
# Returns a Boolean indicating whether content has been registered for this key.
|
53
|
+
def content_for? key
|
54
|
+
(defined? @content) && (@content.key? key)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Evaluates the deferred template content registered with the specified key.
|
58
|
+
#
|
59
|
+
# When the corresponding content_for method is invoked using a control code
|
60
|
+
# directive, the block is set up to append the result to the output buffer
|
61
|
+
# directly.
|
62
|
+
#
|
63
|
+
# key - The Symbol under which to look for template blocks to yield.
|
64
|
+
# opts - A Hash of options to control processing (default: {}):
|
65
|
+
# * :drain - A Boolean indicating whether to drain the key of blocks
|
66
|
+
# after calling them (default: true).
|
67
|
+
#
|
68
|
+
# Examples
|
69
|
+
#
|
70
|
+
# - yield_content :body
|
71
|
+
#
|
72
|
+
# Returns nothing (assuming the content has been captured in the context of control code).
|
73
|
+
def yield_content key, opts = {}
|
74
|
+
if (defined? @content) && (blks = (opts.fetch :drain, true) ? (@content.delete key) : @content[key])
|
75
|
+
blks.map {|b| b.call }.join
|
76
|
+
end
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def asset_uri_scheme
|
81
|
+
if instance_variable_defined? :@asset_uri_scheme
|
82
|
+
@asset_uri_scheme
|
83
|
+
else
|
84
|
+
@asset_uri_scheme = (scheme = @document.attr 'asset-uri-scheme', 'https').nil_or_empty? ? nil : %(#{scheme}:)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def cdn_uri name, version, path = nil
|
89
|
+
[%(#{asset_uri_scheme}#{CDN_BASE}), name, version, path].compact * '/'
|
90
|
+
end
|
91
|
+
|
92
|
+
#--
|
93
|
+
#TODO mix directly into AbstractNode
|
94
|
+
def local_attr name, default_val = nil
|
95
|
+
attr name, default_val, false
|
96
|
+
end
|
97
|
+
|
98
|
+
#--
|
99
|
+
#TODO mix directly into AbstractNode
|
100
|
+
def local_attr? name, default_val = nil
|
101
|
+
attr? name, default_val, false
|
102
|
+
end
|
103
|
+
|
104
|
+
# Retrieve the converted content, wrap it in a `<p>` element if
|
105
|
+
# the content_model equals :simple and return the result.
|
106
|
+
#
|
107
|
+
# Returns the block content as a String, wrapped inside a `<p>` element if
|
108
|
+
# the content_model equals `:simple`.
|
109
|
+
def resolve_content
|
110
|
+
@content_model == :simple ? %(<p>#{content}</p>) : content
|
111
|
+
end
|
112
|
+
|
113
|
+
#--
|
114
|
+
#TODO mix into AbstractBlock directly?
|
115
|
+
def pluck selector = {}, &block
|
116
|
+
quantity = (selector.delete :quantity).to_i
|
117
|
+
if blocks?
|
118
|
+
unless (result = find_by selector, &block).empty?
|
119
|
+
result = result[0..(quantity - 1)] if quantity > 0
|
120
|
+
result.each {|b| b.set_attr 'skip-option', '' }
|
121
|
+
end
|
122
|
+
else
|
123
|
+
result = []
|
124
|
+
end
|
125
|
+
quantity == 1 ? result[0] : result
|
126
|
+
end
|
127
|
+
|
128
|
+
def pluck_first selector = {}, &block
|
129
|
+
pluck selector.merge(quantity: 1), &block
|
130
|
+
end
|
131
|
+
|
132
|
+
def partition_title str
|
133
|
+
::Asciidoctor::Document::Title.new str, separator: (@document.attr 'title-separator')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Retrieves the level-1 section node for the current slide.
|
137
|
+
#
|
138
|
+
# Returns the Asciidoctor::Section for the current slide.
|
139
|
+
def slide
|
140
|
+
node = self
|
141
|
+
until node.context == :section && node.level == 1
|
142
|
+
node = node.parent
|
143
|
+
end
|
144
|
+
node
|
145
|
+
end
|
146
|
+
|
147
|
+
# Resolves the list of build-related roles for this block.
|
148
|
+
#
|
149
|
+
# Consults the build attribute first, then the build option if the build
|
150
|
+
# attribute is not set.
|
151
|
+
#
|
152
|
+
# Also sets the build-initiated attribute on the slide if not previously set.
|
153
|
+
#
|
154
|
+
# Returns an Array of build-related roles or an empty Array if builds are not
|
155
|
+
# enabled on this node.
|
156
|
+
def build_roles
|
157
|
+
if local_attr? :build
|
158
|
+
slide.set_attr 'build-initiated', ''
|
159
|
+
(local_attr :build).split('+').map {|type| BUILD_ROLE_BY_TYPE[type] }
|
160
|
+
elsif option? :build
|
161
|
+
if (_slide = slide).local_attr? 'build-initiated'
|
162
|
+
['build-items']
|
163
|
+
else
|
164
|
+
_slide.set_attr 'build-initiated', ''
|
165
|
+
['build', 'build-items']
|
166
|
+
end
|
167
|
+
else
|
168
|
+
[]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# QUESTION should we wrap in span.line if active but delimiter is not present?
|
173
|
+
# TODO alternate terms for "slice" - part(ition), chunk, segment, split, break
|
174
|
+
def slice_text str, active = nil
|
175
|
+
if (active || (active.nil? && (option? :slice))) && (str.include? ' ')
|
176
|
+
(str.split SliceHintRx).map {|line| %(<span class="line">#{line}</span>) }.join EOL
|
177
|
+
else
|
178
|
+
str
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Retrieves the built-in html5 converter.
|
183
|
+
#
|
184
|
+
# Returns the instance of the Asciidoctor::Converter::Html5Converter
|
185
|
+
# associated with this node.
|
186
|
+
def html5_converter
|
187
|
+
converter.converters[-1]
|
188
|
+
end
|
189
|
+
|
190
|
+
def delegate
|
191
|
+
html5_converter.convert self
|
192
|
+
end
|
193
|
+
|
194
|
+
def include_svg target
|
195
|
+
if (svg = html5_converter.read_svg_contents self, target)
|
196
|
+
# add viewBox attribute if missing
|
197
|
+
unless ViewBoxAttributeRx =~ (start_tag = SvgStartTagRx.match(svg)[0])
|
198
|
+
if (width = start_tag.match WidthAttributeRx) && (width = width[1].to_f) >= 0 &&
|
199
|
+
(height = start_tag.match HeightAttributeRx) && (height = height[1].to_f) >= 0
|
200
|
+
width = width.to_i if width == width.to_i
|
201
|
+
height = height.to_i if height == height.to_i
|
202
|
+
svg = %(<svg viewBox="0 0 #{width} #{height}"#{start_tag[4..-1]}#{svg[start_tag.length..-1]})
|
203
|
+
end
|
204
|
+
end
|
205
|
+
svg
|
206
|
+
else
|
207
|
+
%(<span class="alt">#{local_attr 'alt'}</span>)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def spacer
|
212
|
+
' '
|
213
|
+
end
|
214
|
+
|
215
|
+
def newline
|
216
|
+
if defined? @pretty
|
217
|
+
@pretty ? EOL : nil
|
218
|
+
elsif (@pretty = ::Thread.current[:tilt_current_template].options[:pretty])
|
219
|
+
EOL
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
- _target, _alt, _width, _height, _format, _reload = (local_attr :target), (local_attr :alt), (local_attr :width), (local_attr :height), (local_attr :format), ('true' if option? :reload)
|
2
|
+
/ QUESTION support default image role?
|
3
|
+
figure.image id=id class=[role, ('flux' if option? :flux)]
|
4
|
+
/ TODO support link on image
|
5
|
+
/ TODO append an index/counter to the alt attribute (since it is otherwise shared)
|
6
|
+
- _target.split(/[#,]/).each do |_target|
|
7
|
+
=newline
|
8
|
+
- if _format == 'svg' || (_target.include? '.svg') && ((_svg = option? :inline) || (_obj = option? :interactive))
|
9
|
+
- case
|
10
|
+
- when _svg
|
11
|
+
=include_svg _target
|
12
|
+
- when _obj
|
13
|
+
object type='image/svg+xml' data=(image_uri _target) width=_width height=_height data-reload=_reload
|
14
|
+
/ TODO support fallback option
|
15
|
+
span.alt=_alt
|
16
|
+
- else
|
17
|
+
img src=(image_uri _target) alt=_alt width=_width height=_height data-reload=_reload
|
18
|
+
- if title?
|
19
|
+
=newline
|
20
|
+
figcaption=title
|
21
|
+
- if local_attr? :credit
|
22
|
+
=newline
|
23
|
+
/ QUESTION sould we use data-credit on <figure> element instead?
|
24
|
+
small.credit=local_attr :credit
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- _has_title = title?
|
2
|
+
- content_for :pre
|
3
|
+
- if style == 'source' && (_lang = local_attr :language)
|
4
|
+
/ NOTE class attribute required for highlight.js and prettify
|
5
|
+
/ QUESTION should we add a "highlight" class to pre?
|
6
|
+
pre.source id=(id unless _has_title) : code data-lang=_lang class="language-#{_lang} prettyprint" =content
|
7
|
+
- else
|
8
|
+
/ QUESTION should we add a class to pre?
|
9
|
+
pre id=(id unless _has_title) : code =content
|
10
|
+
- if _has_title
|
11
|
+
figure.listing id=id
|
12
|
+
figcaption=title
|
13
|
+
- yield_content :pre
|
14
|
+
- else
|
15
|
+
- yield_content :pre
|
@@ -0,0 +1,10 @@
|
|
1
|
+
// NOTE if style is not set, then it's a literal paragraph (not a literal block)
|
2
|
+
- _has_title = title?
|
3
|
+
- content_for :pre
|
4
|
+
pre.screen id=(id unless _has_title) class=role : samp =content
|
5
|
+
- if _has_title
|
6
|
+
figure.listing id=id
|
7
|
+
figcaption=title
|
8
|
+
- yield_content :pre
|
9
|
+
- else
|
10
|
+
- yield_content :pre
|