asciidoctor-html5s 0.1.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.adoc +62 -0
- data/asciidoctor-html5s.gemspec +34 -0
- data/data/templates/_attribution.html.slim +4 -0
- data/data/templates/_footer.html.slim +8 -0
- data/data/templates/_footnotes.html.slim +11 -0
- data/data/templates/_hdlist.html.slim +20 -0
- data/data/templates/_header.html.slim +27 -0
- data/data/templates/_qanda.html.slim +12 -0
- data/data/templates/_toc.html.slim +4 -0
- data/data/templates/admonition.html.slim +10 -0
- data/data/templates/audio.html.slim +7 -0
- data/data/templates/colist.html.slim +4 -0
- data/data/templates/dlist.html.slim +13 -0
- data/data/templates/document.html.slim +30 -0
- data/data/templates/embedded.html.slim +5 -0
- data/data/templates/example.html.slim +2 -0
- data/data/templates/floating_title.html.slim +2 -0
- data/data/templates/helpers.rb +665 -0
- data/data/templates/image.html.slim +3 -0
- data/data/templates/inline_anchor.html.slim +12 -0
- data/data/templates/inline_break.html.slim +2 -0
- data/data/templates/inline_button.html.slim +1 -0
- data/data/templates/inline_callout.html.slim +1 -0
- data/data/templates/inline_footnote.html.slim +9 -0
- data/data/templates/inline_image.html.slim +10 -0
- data/data/templates/inline_indexterm.html.slim +2 -0
- data/data/templates/inline_kbd.html.slim +7 -0
- data/data/templates/inline_menu.html.slim +18 -0
- data/data/templates/inline_quoted.html.slim +26 -0
- data/data/templates/listing.html.slim +16 -0
- data/data/templates/literal.html.slim +2 -0
- data/data/templates/olist.html.slim +4 -0
- data/data/templates/open.html.slim +7 -0
- data/data/templates/outline.html.slim +9 -0
- data/data/templates/page_break.html.slim +1 -0
- data/data/templates/paragraph.html.slim +6 -0
- data/data/templates/pass.html.slim +1 -0
- data/data/templates/preamble.html.slim +4 -0
- data/data/templates/quote.html.slim +6 -0
- data/data/templates/section.html.slim +13 -0
- data/data/templates/sidebar.html.slim +4 -0
- data/data/templates/stem.html.slim +2 -0
- data/data/templates/table.html.slim +39 -0
- data/data/templates/thematic_break.html.slim +1 -0
- data/data/templates/toc.html.slim +9 -0
- data/data/templates/ulist.html.slim +11 -0
- data/data/templates/verse.html.slim +7 -0
- data/data/templates/video.html.slim +18 -0
- data/lib/asciidoctor-html5s.rb +1 -0
- data/lib/asciidoctor/html5s.rb +8 -0
- data/lib/asciidoctor/html5s/attached_colist_treeprocessor.rb +24 -0
- data/lib/asciidoctor/html5s/converter.rb +3009 -0
- data/lib/asciidoctor/html5s/version.rb +5 -0
- metadata +226 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c16c90fa30631f549c6e9a6f9677919571dc2d69
|
4
|
+
data.tar.gz: 73ea5fbfd2ee70c5d361e1c8b6d5ef3ef4b6d804
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9fd08e3a180262d33d54d187fe373286306405b65d0a49e5a295e0b5c7116c790dcf678e4a6300d6a20c43ea4f6746463539b292f177e23a30fc4985e2c7a30e
|
7
|
+
data.tar.gz: 87bc0fba47221dd500b4ffe14c1f507aa608d9816ca809b18788c29a3cd8f27eefa49ce488b81122e0f93f3e97669d8faed8a89250d37613d5210f6d46b8ddc2
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright 2014-2017 Jakub Jirutka <jakub@jirutka.cz> and the Asciidoctor Project.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.adoc
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
= Semantic HTML5 Backend For Asciidoctor
|
2
|
+
// custom
|
3
|
+
:gem-name: asciidoctor-html5s
|
4
|
+
:gh-name: jirutka/{gem-name}
|
5
|
+
:gh-branch: master
|
6
|
+
|
7
|
+
ifdef::env-github[]
|
8
|
+
image:https://travis-ci.org/{gh-name}.svg?branch={gh-branch}[Build Status, link="https://travis-ci.org/{gh-name}"]
|
9
|
+
image:https://img.shields.io/gem/v/{gem-name}.svg?style=flat[Gem Version, link="https://rubygems.org/gems/{gem-name}"]
|
10
|
+
endif::env-github[]
|
11
|
+
|
12
|
+
This project provides alternative HTML5 converter (backend) for http://asciidoctor.org/[Asciidoctor] that focuses on correct semantics, accessibility and compatibility with common typographic CSS styles.
|
13
|
+
|
14
|
+
|
15
|
+
== Goals
|
16
|
+
|
17
|
+
* Clean markup with correct HTML5 semantics.
|
18
|
+
* Good accessibility for people with disabilities.
|
19
|
+
* Compatibility with common typographic CSS styles when possible and especially with GitHub and GitLab.
|
20
|
+
* Full standalone converter without fallback to the built-in Asciidoctor converters.
|
21
|
+
* Easy to use and integrate into third-party projects.
|
22
|
+
* Well readable and maintainable code – this should be never sacrificed for performance (I’m looking at you, Asciidoctor!).
|
23
|
+
|
24
|
+
|
25
|
+
== Non-goals
|
26
|
+
|
27
|
+
* Compatibility with existing Asciidoctor CSS styles.
|
28
|
+
|
29
|
+
|
30
|
+
== Requirements
|
31
|
+
|
32
|
+
* https://www.ruby-lang.org/[Ruby] 2.0+ or http://jruby.org/[JRuby] 9.1+
|
33
|
+
* https://rubygems.org/gems/asciidoctor/[Asciidoctor] 1.5.5+
|
34
|
+
* https://rubygems.org/gems/thread_safe/[thread_safe] (not required, but recommended)
|
35
|
+
|
36
|
+
Note: This converter consists of https://github.com/slim-template/slim/[Slim] templates, but they are precompiled into pure Ruby code using https://github.com/jirutka/asciidoctor-templates-compiler/[asciidoctor-templates-compiler], so you don’t need Slim to use it!
|
37
|
+
|
38
|
+
|
39
|
+
== Installation
|
40
|
+
|
41
|
+
Install {gem-name} from Rubygems:
|
42
|
+
|
43
|
+
[source, sh, subs="+attributes"]
|
44
|
+
gem install {gem-name}
|
45
|
+
|
46
|
+
or to get the latest development version:
|
47
|
+
|
48
|
+
[source, sh, subs="+attributes"]
|
49
|
+
gem install --pre {gem-name}
|
50
|
+
|
51
|
+
|
52
|
+
== Usage
|
53
|
+
|
54
|
+
[source, sh, subs="+attributes"]
|
55
|
+
asciidoctor -r {gem-name} -b html5s FILE...
|
56
|
+
|
57
|
+
|
58
|
+
== License
|
59
|
+
|
60
|
+
This project is licensed under http://opensource.org/licenses/MIT/[MIT License].
|
61
|
+
For the full text of the license, see the link:LICENSE[LICENSE] file.
|
62
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.expand_path('lib/asciidoctor/html5s/version', __dir__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'asciidoctor-html5s'
|
6
|
+
s.version = Asciidoctor::Html5s::VERSION
|
7
|
+
s.author = 'Jakub Jirutka'
|
8
|
+
s.email = 'jakub@jirutka.cz'
|
9
|
+
s.homepage = 'https://github.com/jirutka/asciidoctor-html5s'
|
10
|
+
s.license = 'MIT'
|
11
|
+
|
12
|
+
s.summary = 'Semantic HTML5 converter (backend) for Asciidoctor'
|
13
|
+
s.description = <<-EOF
|
14
|
+
This project provides alternative HTML5 converter (backend) for Asciidoctor
|
15
|
+
that focuses on correct semantics, accessibility and compatibility with common
|
16
|
+
typographic CSS styles.
|
17
|
+
EOF
|
18
|
+
|
19
|
+
s.files = Dir['data/**/*', 'lib/**/*', '*.gemspec', 'LICENSE*', 'README*']
|
20
|
+
s.has_rdoc = false
|
21
|
+
|
22
|
+
s.required_ruby_version = '>= 2.0'
|
23
|
+
|
24
|
+
s.add_runtime_dependency 'asciidoctor', '~> 1.5.5'
|
25
|
+
s.add_runtime_dependency 'thread_safe', '~> 0.3.4'
|
26
|
+
|
27
|
+
s.add_development_dependency 'asciidoctor-doctest', '= 2.0.0.beta.4'
|
28
|
+
s.add_development_dependency 'asciidoctor-templates-compiler', '~> 0.1.2'
|
29
|
+
s.add_development_dependency 'bundler', '~> 1.6'
|
30
|
+
s.add_development_dependency 'coderay', '~> 1.1'
|
31
|
+
s.add_development_dependency 'rake', '~> 10.0'
|
32
|
+
s.add_development_dependency 'slim', '~> 3.0'
|
33
|
+
s.add_development_dependency 'slim-htag', '~> 0.1.0'
|
34
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
section.footnotes aria-label='Footnotes' role='doc-endnotes'
|
2
|
+
hr
|
3
|
+
ol.footnotes
|
4
|
+
- footnotes.each do |fn|
|
5
|
+
li.footnote id=(footnote_id fn.index) role='doc-endnote'
|
6
|
+
="#{fn.text} "
|
7
|
+
a.footnote-backref [
|
8
|
+
href="##{footnoteref_id fn.index}"
|
9
|
+
role='doc-backlink'
|
10
|
+
title='Jump to the first occurrence in the text' ]
|
11
|
+
| ↩
|
@@ -0,0 +1,20 @@
|
|
1
|
+
= block_with_title :class=>'hdlist'
|
2
|
+
table
|
3
|
+
- if (attr? :labelwidth) || (attr? :itemwidth)
|
4
|
+
colgroup
|
5
|
+
col style=style_value(width: [(attr :labelwidth), '%'])
|
6
|
+
col style=style_value(width: [(attr :itemwidth), '%'])
|
7
|
+
- items.each do |terms, dd|
|
8
|
+
tr
|
9
|
+
th.hdlist1 class=('strong' if option? 'strong')
|
10
|
+
- terms = [*terms]
|
11
|
+
- terms.each_with_index do |dt, idx|
|
12
|
+
=dt.text
|
13
|
+
- unless idx >= terms.count - 1
|
14
|
+
br
|
15
|
+
td.hdlist2
|
16
|
+
- unless dd.nil?
|
17
|
+
- if dd.text?
|
18
|
+
p =dd.text
|
19
|
+
- if dd.blocks?
|
20
|
+
=dd.content
|
@@ -0,0 +1,27 @@
|
|
1
|
+
- if has_header?
|
2
|
+
- unless notitle
|
3
|
+
h1 =header.title
|
4
|
+
- if [:author, :revnumber, :revdate, :revremark].any? {|a| attr? a }
|
5
|
+
.details
|
6
|
+
- if attr? :author
|
7
|
+
span.author#author =(attr :author)
|
8
|
+
br
|
9
|
+
- if attr? :email
|
10
|
+
span.email#email =sub_macros(attr :email)
|
11
|
+
br
|
12
|
+
- if (authorcount = (attr :authorcount).to_i) > 1
|
13
|
+
- (2..authorcount).each do |idx|
|
14
|
+
span.author id="author#{idx}" =(attr "author_#{idx}")
|
15
|
+
br
|
16
|
+
- if attr? "email_#{idx}"
|
17
|
+
span.email id="email#{idx}" =sub_macros(attr "email_#{idx}")
|
18
|
+
- if attr? :revnumber
|
19
|
+
span#revnumber #{((attr 'version-label') || '').downcase} #{attr :revnumber}#{',' if attr? :revdate}
|
20
|
+
'
|
21
|
+
- if attr? :revdate
|
22
|
+
time#revdate datetime=revdate_iso =(attr :revdate)
|
23
|
+
- if attr? :revremark
|
24
|
+
br
|
25
|
+
span#revremark =(attr :revremark)
|
26
|
+
- if (attr? :toc) && (attr? 'toc-placement', 'auto')
|
27
|
+
include _toc.html
|
@@ -0,0 +1,12 @@
|
|
1
|
+
= block_with_title :class=>'qlist qanda', :role=>'doc-qna'
|
2
|
+
dl.qanda
|
3
|
+
- items.each do |questions, answer|
|
4
|
+
- [*questions].each do |question|
|
5
|
+
dt.qanda-question =question.text
|
6
|
+
- unless answer.nil?
|
7
|
+
dd.qanda-answer
|
8
|
+
- if answer.text?
|
9
|
+
= html_tag_if answer.blocks?, :p
|
10
|
+
=answer.text
|
11
|
+
- if answer.blocks?
|
12
|
+
=answer.content
|
@@ -0,0 +1,10 @@
|
|
1
|
+
- capture
|
2
|
+
h6 =(title || caption)
|
3
|
+
= html_tag_if !blocks?, :p
|
4
|
+
=content
|
5
|
+
- if admonition_aside?
|
6
|
+
aside.admonitionblock id=id class=[(attr :name), role] role=admonition_aria
|
7
|
+
- yield_capture
|
8
|
+
- else
|
9
|
+
section.admonitionblock id=id class=[(attr :name), role] role=admonition_aria
|
10
|
+
- yield_capture
|
@@ -0,0 +1,13 @@
|
|
1
|
+
- case style
|
2
|
+
- when 'qanda'
|
3
|
+
include _qanda.html
|
4
|
+
- when 'horizontal'
|
5
|
+
include _hdlist.html
|
6
|
+
- else
|
7
|
+
= block_with_title :class=>['dlist', style]
|
8
|
+
dl
|
9
|
+
- items.each do |terms, dd|
|
10
|
+
- [*terms].each do |dt|
|
11
|
+
dt =dt.text
|
12
|
+
- unless dd.nil?
|
13
|
+
dd =(print_item_content dd)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
doctype 5
|
2
|
+
html lang=(attr :lang, 'en' unless attr? :nolang)
|
3
|
+
head
|
4
|
+
meta charset=(attr :encoding, 'UTF-8')
|
5
|
+
/[if IE]
|
6
|
+
meta http-equiv="X-UA-Compatible" content="IE=edge"
|
7
|
+
meta name='viewport' content='width=device-width, initial-scale=1.0'
|
8
|
+
meta name='generator' content="Asciidoctor #{attr 'asciidoctor-version'}"
|
9
|
+
= html_meta_if 'application-name', (attr 'app-name')
|
10
|
+
= html_meta_if 'author', (attr :authors)
|
11
|
+
= html_meta_if 'copyright', (attr :copyright)
|
12
|
+
= html_meta_if 'description', (attr :description)
|
13
|
+
= html_meta_if 'keywords', (attr :keywords)
|
14
|
+
title=((doctitle sanitize: true) || (attr 'untitled-label'))
|
15
|
+
= styles_and_scripts
|
16
|
+
- unless (docinfo_content = docinfo).empty?
|
17
|
+
=docinfo_content
|
18
|
+
body [
|
19
|
+
id=id
|
20
|
+
class=[(attr :doctype), ("#{attr 'toc-class'} toc-#{attr 'toc-position', 'left'}" if (attr? 'toc-class') && (attr? :toc) && (attr? 'toc-placement', 'auto'))]
|
21
|
+
style=style_value(max_width: (attr 'max-width')) ]
|
22
|
+
- unless noheader
|
23
|
+
header
|
24
|
+
include _header.html
|
25
|
+
#content =content
|
26
|
+
- unless !footnotes? || (attr? :nofootnotes)
|
27
|
+
include _footnotes.html
|
28
|
+
- unless nofooter
|
29
|
+
footer
|
30
|
+
include _footer.html
|
@@ -0,0 +1,665 @@
|
|
1
|
+
require 'asciidoctor'
|
2
|
+
require 'asciidoctor/html5s'
|
3
|
+
require 'date'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# Needed only in compile-time.
|
7
|
+
require 'slim-htag' if defined? Slim
|
8
|
+
|
9
|
+
if Gem::Version.new(Asciidoctor::VERSION) <= Gem::Version.new('1.5.1')
|
10
|
+
fail 'asciidoctor: FAILED: HTML5/Slim backend needs Asciidoctor >=1.5.2!'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add custom functions to this module that you want to use in your Slim
|
14
|
+
# templates. Within the template you can invoke them as top-level functions
|
15
|
+
# just like in Haml.
|
16
|
+
module Slim::Helpers
|
17
|
+
|
18
|
+
# URIs of external assets.
|
19
|
+
FONT_AWESOME_URI = '//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.1.0/css/font-awesome.min.css'
|
20
|
+
HIGHLIGHTJS_BASE_URI = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'
|
21
|
+
MATHJAX_JS_URI = '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_HTMLorMML'
|
22
|
+
|
23
|
+
# Defaults
|
24
|
+
DEFAULT_HIGHLIGHTJS_THEME = 'github'
|
25
|
+
DEFAULT_SECTNUMLEVELS = 3
|
26
|
+
DEFAULT_TOCLEVELS = 2
|
27
|
+
|
28
|
+
# The MathJax configuration.
|
29
|
+
MATHJAX_CONFIG = {
|
30
|
+
tex2jax: {
|
31
|
+
inlineMath: [::Asciidoctor::INLINE_MATH_DELIMITERS[:latexmath].inspect],
|
32
|
+
displayMath: [::Asciidoctor::BLOCK_MATH_DELIMITERS[:latexmath].inspect],
|
33
|
+
ignoreClass: 'nostem|nolatexmath',
|
34
|
+
},
|
35
|
+
asciimath2jax: {
|
36
|
+
delimiters: [::Asciidoctor::BLOCK_MATH_DELIMITERS[:asciimath].inspect],
|
37
|
+
ignoreClass: 'nostem|noasciimath',
|
38
|
+
}
|
39
|
+
}.to_json
|
40
|
+
|
41
|
+
VOID_ELEMENTS = %w(area base br col command embed hr img input keygen link
|
42
|
+
meta param source track wbr)
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
# Captures the given block for later yield.
|
47
|
+
#
|
48
|
+
# @example Basic capture usage.
|
49
|
+
# - capture
|
50
|
+
# img src=image_uri
|
51
|
+
# - if title?
|
52
|
+
# figure.image
|
53
|
+
# - yield_capture
|
54
|
+
# figcaption =captioned_title
|
55
|
+
# - else
|
56
|
+
# - yield_capture
|
57
|
+
#
|
58
|
+
# @example Capture with passing parameters.
|
59
|
+
# - capture do |id|
|
60
|
+
# img src=image_uri
|
61
|
+
# - if title?
|
62
|
+
# figure id=@id
|
63
|
+
# - yield_capture
|
64
|
+
# figcaption =caption
|
65
|
+
# - else
|
66
|
+
# - yield_capture @id
|
67
|
+
#
|
68
|
+
# @see yield_capture
|
69
|
+
def capture(&block)
|
70
|
+
@_html5s_capture = block
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Yields the captured block (see {#capture}).
|
76
|
+
#
|
77
|
+
# @param *params parameters to pass to the block.
|
78
|
+
# @return A content of the captured block.
|
79
|
+
# @see capture
|
80
|
+
def yield_capture(*params)
|
81
|
+
@_html5s_capture.call(*params) if @_html5s_capture
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Creates an HTML tag with the given name and optionally attributes. Can take
|
86
|
+
# a block that will run between the opening and closing tags.
|
87
|
+
#
|
88
|
+
# @param name [#to_s] the name of the tag.
|
89
|
+
# @param attributes [Hash] (default: {})
|
90
|
+
# @param content [#to_s] the content; +nil+ to call the block. (default: nil).
|
91
|
+
# @yield The block of Slim/HTML code within the tag (optional).
|
92
|
+
# @return [String] a rendered HTML element.
|
93
|
+
#
|
94
|
+
def html_tag(name, attributes = {}, content = nil)
|
95
|
+
attrs = attributes.inject([]) do |attrs, (k, v)|
|
96
|
+
next attrs if !v || v.nil_or_empty?
|
97
|
+
v = v.compact.join(' ') if v.is_a? Array
|
98
|
+
attrs << (v == true ? k : %(#{k}="#{v}"))
|
99
|
+
end
|
100
|
+
attrs_str = attrs.empty? ? '' : attrs.join(' ').prepend(' ')
|
101
|
+
|
102
|
+
if VOID_ELEMENTS.include? name.to_s
|
103
|
+
%(<#{name}#{attrs_str}>)
|
104
|
+
else
|
105
|
+
content ||= yield if block_given?
|
106
|
+
%(<#{name}#{attrs_str}>#{content}</#{name}>)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Conditionally wraps a block in an element. If condition is +true+ then it
|
112
|
+
# renders the specified tag with optional attributes and the given
|
113
|
+
# block inside, otherwise it just renders the block.
|
114
|
+
#
|
115
|
+
# For example:
|
116
|
+
#
|
117
|
+
# = html_tag_if link?, 'a', {class: 'image', href: (attr :link)}
|
118
|
+
# img src='./img/tux.png'
|
119
|
+
#
|
120
|
+
# will produce:
|
121
|
+
#
|
122
|
+
# <a href="http://example.org" class="image">
|
123
|
+
# <img src="./img/tux.png">
|
124
|
+
# </a>
|
125
|
+
#
|
126
|
+
# if +link?+ is truthy, and just
|
127
|
+
#
|
128
|
+
# <img src="./img/tux.png">
|
129
|
+
#
|
130
|
+
# otherwise.
|
131
|
+
#
|
132
|
+
# @param condition [Boolean] the condition to test to determine whether to
|
133
|
+
# render the enclosing tag.
|
134
|
+
# @param name (see #html_tag)
|
135
|
+
# @param attributes (see #html_tag)
|
136
|
+
# @param content (see #html_tag)
|
137
|
+
# @yield (see #html_tag)
|
138
|
+
# @return [String] a rendered HTML fragment.
|
139
|
+
#
|
140
|
+
def html_tag_if(condition, name, attributes = {}, content = nil, &block)
|
141
|
+
if condition
|
142
|
+
html_tag name, attributes, content, &block
|
143
|
+
else
|
144
|
+
content || yield
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Wraps a block in a div element with the specified class and optionally
|
150
|
+
# the node's +id+ and +role+(s). If the node's +title+ is not empty, then a
|
151
|
+
# nested div with the class "title" and the title's content is added as well.
|
152
|
+
#
|
153
|
+
# @example When @id, @role and @title attributes are set.
|
154
|
+
# = block_with_title :class=>['quoteblock', 'center']
|
155
|
+
# blockquote =content
|
156
|
+
#
|
157
|
+
# <section id="myid" class="quoteblock center myrole1 myrole2">
|
158
|
+
# <h6>Block Title</h6>
|
159
|
+
# <blockquote>Lorem ipsum</blockquote>
|
160
|
+
# </section>
|
161
|
+
#
|
162
|
+
# @example When @id, @role and @title attributes are empty.
|
163
|
+
# = block_with_title :class=>'quoteblock center', :style=>style_value(float: 'left')
|
164
|
+
# blockquote =content
|
165
|
+
#
|
166
|
+
# <div class="quoteblock center" style="float: left;">
|
167
|
+
# <blockquote>Lorem ipsum</blockquote>
|
168
|
+
# </div>
|
169
|
+
#
|
170
|
+
# @example When shorthand style for class attribute is used.
|
171
|
+
# = block_with_title 'quoteblock center'
|
172
|
+
# blockquote =content
|
173
|
+
#
|
174
|
+
# <div class="quoteblock center">
|
175
|
+
# <blockquote>Lorem ipsum</blockquote>
|
176
|
+
# </div>
|
177
|
+
#
|
178
|
+
# @param attrs [Hash, String] the tag's attributes as Hash),
|
179
|
+
# or the tag's class if it's not a Hash.
|
180
|
+
# @param title [String, nil] the title.
|
181
|
+
# @yield The block of Slim/HTML code within the tag (optional).
|
182
|
+
# @return [String] a rendered HTML fragment.
|
183
|
+
#
|
184
|
+
def block_with_title(attrs = {}, title = @title, &block)
|
185
|
+
if (klass = attrs[:class]).is_a? String
|
186
|
+
klass = klass.split(' ')
|
187
|
+
end
|
188
|
+
attrs[:class] = [klass, role].flatten.uniq
|
189
|
+
attrs[:id] = id
|
190
|
+
|
191
|
+
if title.nil_or_empty?
|
192
|
+
# XXX quick hack
|
193
|
+
nested = is_a?(::Asciidoctor::List) &&
|
194
|
+
(parent.is_a?(::Asciidoctor::ListItem) || parent.is_a?(::Asciidoctor::List))
|
195
|
+
html_tag_if !nested, :div, attrs, yield
|
196
|
+
else
|
197
|
+
html_tag :section, attrs do
|
198
|
+
[html_tag(:h6, {}, title), yield].join("\n")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def block_with_caption(position = :bottom, attrs = {}, &block)
|
204
|
+
if (klass = attrs[:class]).is_a? String
|
205
|
+
klass = klass.split(' ')
|
206
|
+
end
|
207
|
+
attrs[:class] = [klass, role].flatten.uniq
|
208
|
+
attrs[:id] = id
|
209
|
+
|
210
|
+
if title.nil_or_empty?
|
211
|
+
html_tag :div, attrs, yield
|
212
|
+
else
|
213
|
+
html_tag :figure, attrs do
|
214
|
+
ary = [yield, html_tag(:figcaption) { captioned_title }]
|
215
|
+
ary.reverse! if position == :top
|
216
|
+
ary.compact.join("\n")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
##
|
222
|
+
# Delimite the given equation as a STEM of the specified type.
|
223
|
+
#
|
224
|
+
# @param equation [String] the equation to delimite.
|
225
|
+
# @param type [#to_sym] the type of the STEM renderer (latexmath, or asciimath).
|
226
|
+
# @return [String] the delimited equation.
|
227
|
+
#
|
228
|
+
def delimit_stem(equation, type)
|
229
|
+
if is_a? ::Asciidoctor::Block
|
230
|
+
open, close = ::Asciidoctor::BLOCK_MATH_DELIMITERS[type.to_sym]
|
231
|
+
else
|
232
|
+
open, close = ::Asciidoctor::INLINE_MATH_DELIMITERS[type.to_sym]
|
233
|
+
end
|
234
|
+
|
235
|
+
if !equation.start_with?(open) || !equation.end_with?(close)
|
236
|
+
equation = [open, equation, close].join
|
237
|
+
end
|
238
|
+
equation
|
239
|
+
end
|
240
|
+
|
241
|
+
##
|
242
|
+
# Formats the given hash as CSS declarations for an inline style.
|
243
|
+
#
|
244
|
+
# @example
|
245
|
+
# style_value(text_align: 'right', float: 'left')
|
246
|
+
# => "text-align: right; float: left;"
|
247
|
+
#
|
248
|
+
# style_value(text_align: nil, float: 'left')
|
249
|
+
# => "float: left;"
|
250
|
+
#
|
251
|
+
# style_value(width: [90, '%'], height: '50px')
|
252
|
+
# => "width: 90%; height: 50px;"
|
253
|
+
#
|
254
|
+
# style_value(width: ['120px', 'px'])
|
255
|
+
# => "width: 90px;"
|
256
|
+
#
|
257
|
+
# style_value(width: [nil, 'px'])
|
258
|
+
# => nil
|
259
|
+
#
|
260
|
+
# @param declarations [Hash]
|
261
|
+
# @return [String, nil]
|
262
|
+
#
|
263
|
+
def style_value(declarations)
|
264
|
+
decls = []
|
265
|
+
|
266
|
+
declarations.each do |prop, value|
|
267
|
+
next if value.nil?
|
268
|
+
|
269
|
+
if value.is_a? Array
|
270
|
+
value, unit = value
|
271
|
+
next if value.nil?
|
272
|
+
value = value.to_s + unit unless value.end_with? unit
|
273
|
+
end
|
274
|
+
prop = prop.to_s.gsub('_', '-')
|
275
|
+
decls << "#{prop}: #{value}"
|
276
|
+
end
|
277
|
+
|
278
|
+
decls.empty? ? nil : decls.join('; ') + ';'
|
279
|
+
end
|
280
|
+
|
281
|
+
def urlize(*segments)
|
282
|
+
path = segments * '/'
|
283
|
+
if path.start_with? '//'
|
284
|
+
@_uri_scheme ||= document.attr('asset-uri-scheme', 'https')
|
285
|
+
path = "#{@_uri_scheme}:#{path}" unless @_uri_scheme.empty?
|
286
|
+
end
|
287
|
+
normalize_web_path path
|
288
|
+
end
|
289
|
+
|
290
|
+
|
291
|
+
##
|
292
|
+
# Gets the value of the specified attribute in this node.
|
293
|
+
#
|
294
|
+
# This is just an alias for +attr+ method with disabled _inherit_ to make it
|
295
|
+
# more clear.
|
296
|
+
#
|
297
|
+
# @param name [String, Symbol] the name of the attribute to lookup.
|
298
|
+
# @param default_val the value to return if the attribute is not found.
|
299
|
+
# @return value of the attribute or +default_val+ if not found.
|
300
|
+
#
|
301
|
+
def local_attr(name, default_val = nil)
|
302
|
+
attr(name, default_val, false)
|
303
|
+
end
|
304
|
+
|
305
|
+
##
|
306
|
+
# Checks if the attribute is defined on this node, optionally performing
|
307
|
+
# a comparison of its value if +expect_val+ is not nil.
|
308
|
+
#
|
309
|
+
# This is just an alias for +attr?+ method with disabled _inherit_ to make it
|
310
|
+
# more clear.
|
311
|
+
#
|
312
|
+
# @param name [String, Symbol] the name of the attribute to lookup.
|
313
|
+
# @param default_val the expected value of the attribute.
|
314
|
+
# @return [Boolean] whether the attribute exists and, if +expect_val+ is
|
315
|
+
# specified, whether the value of the attribute matches the +expect_val+.
|
316
|
+
#
|
317
|
+
def local_attr?(name, expect_val = nil)
|
318
|
+
attr?(name, expect_val, false)
|
319
|
+
end
|
320
|
+
|
321
|
+
##
|
322
|
+
# @param index [Integer] the footnote's index.
|
323
|
+
# @return [String] footnote id to be used in a link.
|
324
|
+
def footnote_id(index = local_attr(:index))
|
325
|
+
"_footnote_#{index}"
|
326
|
+
end
|
327
|
+
|
328
|
+
##
|
329
|
+
# @param index (see #footnote_id)
|
330
|
+
# @return [String] footnoteref id to be used in a link.
|
331
|
+
def footnoteref_id(index = local_attr(:index))
|
332
|
+
"_footnoteref_#{index}"
|
333
|
+
end
|
334
|
+
|
335
|
+
def icons?
|
336
|
+
document.attr? :icons
|
337
|
+
end
|
338
|
+
|
339
|
+
def font_icons?
|
340
|
+
document.attr? :icons, 'font'
|
341
|
+
end
|
342
|
+
|
343
|
+
def nowrap?
|
344
|
+
'nowrap' if !document.attr?(:prewrap) || option?('nowrap')
|
345
|
+
end
|
346
|
+
|
347
|
+
def print_item_content(item)
|
348
|
+
wrap = item.blocks? && !item.blocks.all? { |b| b.is_a? ::Asciidoctor::List }
|
349
|
+
[ (html_tag_if(wrap, :p) { item.text } if item.text?), item.content ].join
|
350
|
+
end
|
351
|
+
|
352
|
+
##
|
353
|
+
# Returns corrected section level.
|
354
|
+
#
|
355
|
+
# @param sec [Asciidoctor::Section] the section node (default: self).
|
356
|
+
# @return [Integer]
|
357
|
+
#
|
358
|
+
def section_level(sec = self)
|
359
|
+
@_section_level ||= (sec.level == 0 && sec.special) ? 1 : sec.level
|
360
|
+
end
|
361
|
+
|
362
|
+
##
|
363
|
+
# Returns the captioned section's title, optionally numbered.
|
364
|
+
#
|
365
|
+
# @param sec [Asciidoctor::Section] the section node (default: self).
|
366
|
+
# @return [String]
|
367
|
+
#
|
368
|
+
def section_title(sec = self)
|
369
|
+
sectnumlevels = document.attr(:sectnumlevels, DEFAULT_SECTNUMLEVELS).to_i
|
370
|
+
|
371
|
+
if sec.numbered && !sec.caption && sec.level <= sectnumlevels
|
372
|
+
[sec.sectnum, sec.captioned_title].join(' ')
|
373
|
+
else
|
374
|
+
sec.captioned_title
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def link_rel
|
379
|
+
'noopener' if option?('noopener') || attr(:window) == '_blank'
|
380
|
+
end
|
381
|
+
|
382
|
+
#--------------------------------------------------------
|
383
|
+
# block_admonition
|
384
|
+
#
|
385
|
+
|
386
|
+
##
|
387
|
+
# @return [Boolean] should be this admonition wrapped in aside element?
|
388
|
+
def admonition_aside?
|
389
|
+
%w[note tip].include? attr(:name)
|
390
|
+
end
|
391
|
+
|
392
|
+
##
|
393
|
+
# @return [String, nil] WAI-ARIA role of this admonition.
|
394
|
+
def admonition_aria
|
395
|
+
case attr(:name)
|
396
|
+
when 'note'
|
397
|
+
'note' # https://www.w3.org/TR/wai-aria/roles#note
|
398
|
+
when 'tip'
|
399
|
+
'doc-tip' # https://www.w3.org/TR/dpub-aria-1.0/#doc-tip
|
400
|
+
when 'caution', 'important', 'warning'
|
401
|
+
'doc-notice' # https://www.w3.org/TR/dpub-aria-1.0/#doc-notice
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
#--------------------------------------------------------
|
406
|
+
# block_listing
|
407
|
+
#
|
408
|
+
|
409
|
+
##
|
410
|
+
# @return [String] a canonical name of the source-highlighter to be used as
|
411
|
+
# a style class.
|
412
|
+
def highlighter
|
413
|
+
@_highlighter ||=
|
414
|
+
case (highlighter = document.attr('source-highlighter'))
|
415
|
+
when 'coderay'; 'CodeRay'
|
416
|
+
when 'highlight.js'; 'highlightjs'
|
417
|
+
else highlighter
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
##
|
422
|
+
# Returns the callout list attached to this listing node, or +nil+ if none.
|
423
|
+
#
|
424
|
+
# Note: This variable is set by extension
|
425
|
+
# {Asciidoctor::Html5s::AttachedColistTreeprocessor}.
|
426
|
+
#
|
427
|
+
# @return [Asciidoctor::List, nil]
|
428
|
+
def callout_list
|
429
|
+
@html5s_colist
|
430
|
+
end
|
431
|
+
|
432
|
+
def source_lang
|
433
|
+
local_attr :language, false
|
434
|
+
end
|
435
|
+
|
436
|
+
#--------------------------------------------------------
|
437
|
+
# block_open
|
438
|
+
#
|
439
|
+
|
440
|
+
##
|
441
|
+
# Returns +true+ if an abstract block is allowed in this document type,
|
442
|
+
# otherwise prints warning and returns +false+.
|
443
|
+
def abstract_allowed?
|
444
|
+
if result = (parent == document && document.doctype == 'book')
|
445
|
+
puts 'asciidoctor: WARNING: abstract block cannot be used in a document
|
446
|
+
without a title when doctype is book. Excluding block content.'
|
447
|
+
end
|
448
|
+
!result
|
449
|
+
end
|
450
|
+
|
451
|
+
##
|
452
|
+
# Returns +true+ if a partintro block is allowed in this context, otherwise
|
453
|
+
# prints warning and returns +false+.
|
454
|
+
def partintro_allowed?
|
455
|
+
if result = (level != 0 || parent.context != :section || document.doctype != 'book')
|
456
|
+
puts "asciidoctor: ERROR: partintro block can only be used when doctype
|
457
|
+
is book and it's a child of a book part. Excluding block content."
|
458
|
+
end
|
459
|
+
!result
|
460
|
+
end
|
461
|
+
|
462
|
+
#--------------------------------------------------------
|
463
|
+
# block_table
|
464
|
+
#
|
465
|
+
|
466
|
+
def autowidth?
|
467
|
+
option? :autowidth
|
468
|
+
end
|
469
|
+
|
470
|
+
def spread?
|
471
|
+
if !autowidth? || local_attr?('width')
|
472
|
+
'spread' if attr? :tablepcwidth, 100
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
#--------------------------------------------------------
|
477
|
+
# block_video
|
478
|
+
#
|
479
|
+
|
480
|
+
# @return [Boolean] +true+ if the video should be embedded in an iframe.
|
481
|
+
def video_iframe?
|
482
|
+
['vimeo', 'youtube'].include? attr(:poster)
|
483
|
+
end
|
484
|
+
|
485
|
+
def video_uri
|
486
|
+
case attr(:poster, '').to_sym
|
487
|
+
when :vimeo
|
488
|
+
params = {
|
489
|
+
autoplay: (1 if option? 'autoplay'),
|
490
|
+
loop: (1 if option? 'loop')
|
491
|
+
}
|
492
|
+
start_anchor = "#at=#{attr :start}" if attr? :start
|
493
|
+
"//player.vimeo.com/video/#{attr :target}#{start_anchor}#{url_query params}"
|
494
|
+
|
495
|
+
when :youtube
|
496
|
+
video_id, list_id = attr(:target).split('/', 2)
|
497
|
+
params = {
|
498
|
+
rel: 0,
|
499
|
+
start: (attr :start),
|
500
|
+
end: (attr :end),
|
501
|
+
list: (attr :list, list_id),
|
502
|
+
autoplay: (1 if option? 'autoplay'),
|
503
|
+
loop: (1 if option? 'loop'),
|
504
|
+
controls: (0 if option? 'nocontrols')
|
505
|
+
}
|
506
|
+
"//www.youtube.com/embed/#{video_id}#{url_query params}"
|
507
|
+
else
|
508
|
+
anchor = [attr(:start), attr(:end)].join(',').chomp(',')
|
509
|
+
anchor.prepend('#t=') unless anchor.empty?
|
510
|
+
media_uri "#{attr :target}#{anchor}"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Formats URL query parameters.
|
515
|
+
def url_query(params)
|
516
|
+
str = params.map { |k, v|
|
517
|
+
next if v.nil? || v.to_s.empty?
|
518
|
+
[k, v] * '='
|
519
|
+
}.compact.join('&')
|
520
|
+
|
521
|
+
str.prepend('?') unless str.empty?
|
522
|
+
end
|
523
|
+
|
524
|
+
#--------------------------------------------------------
|
525
|
+
# document
|
526
|
+
#
|
527
|
+
|
528
|
+
##
|
529
|
+
# @return [String, nil] the revision date in ISO 8601, or nil if not
|
530
|
+
# available or in invalid format.
|
531
|
+
def revdate_iso
|
532
|
+
::Date.parse(revdate).iso8601
|
533
|
+
rescue ArgumentError
|
534
|
+
nil
|
535
|
+
end
|
536
|
+
|
537
|
+
##
|
538
|
+
# Returns HTML meta tag if the given +content+ is not +nil+.
|
539
|
+
#
|
540
|
+
# @param name [#to_s] the name for the metadata.
|
541
|
+
# @param content [#to_s, nil] the value of the metadata, or +nil+.
|
542
|
+
# @return [String, nil] the meta tag, or +nil+ if the +content+ is +nil+.
|
543
|
+
#
|
544
|
+
def html_meta_if(name, content)
|
545
|
+
%(<meta name="#{name}" content="#{content}">) if content
|
546
|
+
end
|
547
|
+
|
548
|
+
# Returns formatted style/link and script tags for header.
|
549
|
+
def styles_and_scripts
|
550
|
+
scripts = []
|
551
|
+
styles = []
|
552
|
+
tags = []
|
553
|
+
|
554
|
+
stylesheet = attr :stylesheet
|
555
|
+
stylesdir = attr :stylesdir, ''
|
556
|
+
default_style = ::Asciidoctor::DEFAULT_STYLESHEET_KEYS.include? stylesheet
|
557
|
+
linkcss = attr?(:linkcss) || safe >= ::Asciidoctor::SafeMode::SECURE
|
558
|
+
ss = ::Asciidoctor::Stylesheets.instance
|
559
|
+
|
560
|
+
if linkcss
|
561
|
+
path = default_style ? ::Asciidoctor::DEFAULT_STYLESHEET_NAME : stylesheet
|
562
|
+
styles << { href: [stylesdir, path] }
|
563
|
+
elsif default_style
|
564
|
+
styles << { text: ss.primary_stylesheet_data }
|
565
|
+
else
|
566
|
+
styles << { text: read_asset(normalize_system_path(stylesheet, stylesdir), true) }
|
567
|
+
end
|
568
|
+
|
569
|
+
if attr? :icons, 'font'
|
570
|
+
if attr? 'iconfont-remote'
|
571
|
+
styles << { href: attr('iconfont-cdn', FONT_AWESOME_URI) }
|
572
|
+
else
|
573
|
+
styles << { href: [stylesdir, "#{attr 'iconfont-name', 'font-awesome'}.css"] }
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
if attr? 'stem'
|
578
|
+
scripts << { src: MATHJAX_JS_URI }
|
579
|
+
scripts << { type: 'text/x-mathjax-config', text: "MathJax.Hub.Config(#{MATHJAX_CONFIG});" }
|
580
|
+
end
|
581
|
+
|
582
|
+
case attr 'source-highlighter'
|
583
|
+
when 'coderay'
|
584
|
+
if attr('coderay-css', 'class') == 'class'
|
585
|
+
if linkcss
|
586
|
+
styles << { href: [stylesdir, ss.coderay_stylesheet_name] }
|
587
|
+
else
|
588
|
+
styles << { text: ss.coderay_stylesheet_data }
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
when 'highlightjs'
|
593
|
+
hjs_base = attr :highlightjsdir, HIGHLIGHTJS_BASE_URI
|
594
|
+
hjs_theme = attr 'highlightjs-theme', DEFAULT_HIGHLIGHTJS_THEME
|
595
|
+
|
596
|
+
scripts << { src: [hjs_base, 'highlight.min.js'] }
|
597
|
+
scripts << { src: [hjs_base, 'lang/common.min.js'] }
|
598
|
+
scripts << { text: 'hljs.initHighlightingOnLoad()' }
|
599
|
+
styles << { href: [hjs_base, "styles/#{hjs_theme}.min.css"] }
|
600
|
+
end
|
601
|
+
|
602
|
+
styles.each do |item|
|
603
|
+
if item.key?(:text)
|
604
|
+
tags << html_tag(:style) { item[:text] }
|
605
|
+
else
|
606
|
+
tags << html_tag(:link, rel: 'stylesheet', href: urlize(*item[:href]))
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
scripts.each do |item|
|
611
|
+
if item.key? :text
|
612
|
+
tags << html_tag(:script, type: item[:type]) { item[:text] }
|
613
|
+
else
|
614
|
+
tags << html_tag(:script, type: item[:type], src: urlize(*item[:src]))
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
tags.join("\n")
|
619
|
+
end
|
620
|
+
|
621
|
+
#--------------------------------------------------------
|
622
|
+
# inline_anchor
|
623
|
+
#
|
624
|
+
|
625
|
+
# @return [String] text of the xref anchor.
|
626
|
+
def xref_text
|
627
|
+
str =
|
628
|
+
if text
|
629
|
+
text
|
630
|
+
elsif (path = local_attr :path)
|
631
|
+
path
|
632
|
+
elsif document.respond_to? :catalog # Asciidoctor >=1.5.6
|
633
|
+
ref = document.catalog[:refs][attr :refid]
|
634
|
+
if ref.kind_of? Asciidoctor::AbstractNode
|
635
|
+
ref.xreftext((@_xrefstyle ||= document.attributes['xrefstyle']))
|
636
|
+
end
|
637
|
+
else # Asciidoctor < 1.5.6
|
638
|
+
document.references[:ids][attr :refid || target]
|
639
|
+
end
|
640
|
+
(str || "[#{attr :refid}]").tr_s("\n", ' ')
|
641
|
+
end
|
642
|
+
|
643
|
+
# @return [String, nil] text of the bibref anchor, or +nil+ if not found.
|
644
|
+
def bibref_text
|
645
|
+
if document.respond_to? :catalog # Asciidoctor >=1.5.6
|
646
|
+
# NOTE: Technically it should be `reftext`, but subs have already been applied to text.
|
647
|
+
text
|
648
|
+
else
|
649
|
+
"[#{target}]"
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
#--------------------------------------------------------
|
654
|
+
# inline_image
|
655
|
+
#
|
656
|
+
|
657
|
+
# @return [Array] style classes for a Font Awesome icon.
|
658
|
+
def icon_fa_classes
|
659
|
+
[ "fa fa-#{target}",
|
660
|
+
("fa-#{attr :size}" if attr? :size),
|
661
|
+
("fa-rotate-#{attr :rotate}" if attr? :rotate),
|
662
|
+
("fa-flip-#{attr :flip}" if attr? :flip)
|
663
|
+
].compact
|
664
|
+
end
|
665
|
+
end
|