├── CHANGELOG
├── makefile
├── test
├── helper.rb
└── all.rb
├── hypertext.gemspec
├── CONTRIBUTING
├── LICENSE
├── README.md
└── lib
├── hypertext
└── dsl.rb
└── hypertext.rb
/CHANGELOG:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test
2 |
3 | test:
4 | cutest -r ./test/helper.rb ./test/*.rb
5 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require_relative "../lib/hypertext"
2 | require_relative "../lib/hypertext/dsl"
3 |
--------------------------------------------------------------------------------
/hypertext.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = "hypertext"
3 | s.version = "0.0.4"
4 | s.summary = "Hypertext authoring"
5 | s.description = "Hypertext authoring with Ruby"
6 | s.authors = ["Michel Martens"]
7 | s.email = ["michel@soveran.com"]
8 | s.homepage = "https://github.com/soveran/hypertext"
9 | s.license = "MIT"
10 |
11 | s.files = `git ls-files`.split("\n")
12 |
13 | s.add_development_dependency "cutest"
14 | end
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING:
--------------------------------------------------------------------------------
1 | This code tries to solve a particular problem with a very simple
2 | implementation. We try to keep the code to a minimum while making
3 | it as clear as possible. The design is very likely finished, and
4 | if some feature is missing it is possible that it was left out on
5 | purpose. That said, new usage patterns may arise, and when that
6 | happens we are ready to adapt if necessary.
7 |
8 | A good first step for contributing is to meet us on IRC and discuss
9 | ideas. We spend a lot of time on #lesscode at freenode, always ready
10 | to talk about code and simplicity. If connecting to IRC is not an
11 | option, you can create an issue explaining the proposed change and
12 | a use case. We pay a lot of attention to use cases, because our
13 | goal is to keep the code base simple. Usually the result of a
14 | conversation is the creation of a different tool.
15 |
16 | Please don't start the conversation with a pull request. The code
17 | should come at last, and even though it may help to convey an idea,
18 | more often than not it draws the attention to a particular
19 | implementation.
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Michel Martens
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Hypertext
2 | =========
3 |
4 | Hypertext authoring
5 |
6 | Description
7 | -----------
8 |
9 | Hypertext allows you to write HTML from Ruby.
10 |
11 | Usage
12 | -----
13 |
14 | A basic example would look like this:
15 |
16 | ```ruby
17 | html = Hypertext.new do |h|
18 | h.tag :div, "data-index-number" => 123, class: "greeting" do
19 | h.tag :h1 do
20 | h.text "hello world"
21 | end
22 |
23 | h.tag :hr
24 |
25 | h.tag :p do
26 | h.text "nice to meet you"
27 | end
28 | end
29 | end
30 |
31 | puts html.to_s
32 |
33 | #
34 | #
35 | # hello world
36 | #
37 | #
38 | #
39 | # nice to meet you
40 | #
41 | #
42 | ```
43 |
44 | DSL
45 | ---
46 |
47 | As an experimental feature, Hypertext provides a DSL for
48 | describing an HTML document in a way that resembles
49 | [Markaby](https://github.com/markaby/markaby).
50 |
51 | ```ruby
52 | require "hypertext"
53 | require "hypertext/dsl"
54 |
55 | person_name = "Foo Bar"
56 |
57 | html = Hypertext::DSL.new do
58 | form action: "/", method: "post" do
59 | input name: "person[name]", value: person_name
60 | input type: "submit"
61 | end
62 | end
63 |
64 | puts html.to_s
65 |
66 | #
70 | ```
71 |
72 | Installation
73 | ------------
74 |
75 | ```
76 | $ gem install hypertext
77 | ```
78 |
--------------------------------------------------------------------------------
/test/all.rb:
--------------------------------------------------------------------------------
1 | scope "Hypertext" do
2 | test "append" do
3 | expected = "hello → world\n"
4 |
5 | ht = Hypertext.new
6 | ht.append "hello → world"
7 |
8 | assert_equal expected, ht.to_s
9 | end
10 |
11 | test "text" do
12 | expected = "hello world\n"
13 |
14 | ht = Hypertext.new
15 | ht.text "hello world"
16 |
17 | assert_equal expected, ht.to_s
18 | end
19 |
20 | test "self-closing tags" do
21 | expected = " \n"
22 |
23 | ht = Hypertext.new
24 | ht.tag :br
25 |
26 | assert_equal expected, ht.to_s
27 | end
28 |
29 | test "opening and closing tags" do
30 | expected = "\n hello world\n\n"
31 |
32 | ht = Hypertext.new
33 | ht.tag :span do
34 | ht.text "hello world"
35 | end
36 |
37 | assert_equal expected, ht.to_s
38 | end
39 |
40 | test "nested tags" do
41 | expected = "
\n
\n hello world\n
\n
\n"
42 |
43 | ht = Hypertext.new
44 | ht.tag :div do
45 | ht.tag :p do
46 | ht.text "hello world"
47 | end
48 | end
49 |
50 | assert_equal expected, ht.to_s
51 | end
52 |
53 | test "passing a block to Hypertext.new" do
54 | expected = " \n"
55 |
56 | ht = Hypertext.new do |ht|
57 | ht.tag :br
58 | end
59 |
60 | assert_equal expected, ht.to_s
61 | end
62 |
63 | test "tags with attributes" do
64 | expected = "\n"
65 |
66 | ht = Hypertext.new do |ht|
67 | ht.tag :input, :type => "text", :name => "person[name]", :value => "Foo"
68 | end
69 |
70 | assert_equal expected, ht.to_s
71 | end
72 |
73 | test "custom indentation" do
74 | expected = "
\n....hello world\n
\n"
75 |
76 | ht = Hypertext.new do |ht|
77 | ht.tag :p do
78 | ht.text "hello world"
79 | end
80 | end
81 |
82 | assert_equal expected, ht.to_s("....")
83 | end
84 | end
85 |
86 | scope "Hypertext::DSL" do
87 | test do
88 | expected = "\n \n hello world\n \n\n"
89 |
90 | ht = Hypertext::DSL.new do
91 | head do
92 | title do
93 | text "hello world"
94 | end
95 | end
96 | end
97 |
98 | assert_equal expected, ht.to_s
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/hypertext/dsl.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | #
3 | # Copyright (c) 2021 Michel Martens
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.
22 |
23 | class Hypertext
24 | class DSL
25 | TAGS = [:a, :abbr, :address, :area, :article, :aside,
26 | :audio, :b, :base, :bdi, :bdo, :blockquote, :body, :br,
27 | :button, :canvas, :caption, :cite, :code, :col, :colgroup,
28 | :data, :datalist, :dd, :del, :details, :dfn, :dialog, :div,
29 | :dl, :dt, :em, :embed, :fieldset, :figcaption, :figure,
30 | :footer, :form, :h1, :h2, :h3, :h4, :h5, :h6, :head,
31 | :header, :hgroup, :hr, :html, :i, :iframe, :img, :input,
32 | :ins, :kbd, :label, :legend, :li, :link, :main, :map,
33 | :mark, :meta, :meter, :nav, :noscript, :object, :ol,
34 | :optgroup, :option, :output, :p, :param, :picture, :pre,
35 | :progress, :q, :rb, :rp, :rt, :rtc, :ruby, :s, :samp,
36 | :script, :section, :select, :slot, :small, :source, :span,
37 | :strong, :style, :sub, :summary, :sup, :table, :tbody, :td,
38 | :template, :textarea, :tfoot, :th, :thead, :time, :title,
39 | :tr, :track, :u, :ul, :var, :video, :wbr]
40 |
41 | def initialize(&block)
42 | @ht = Hypertext.new
43 | instance_eval(&block)
44 | end
45 |
46 | TAGS.each do |tag_name|
47 | define_method(tag_name) do |attributes = {}, &block|
48 | @ht.tag(tag_name, attributes, &block)
49 | end
50 | end
51 |
52 | def append(content)
53 | @ht.append(content)
54 | end
55 |
56 | def text(content)
57 | @ht.text(content)
58 | end
59 |
60 | def to_a
61 | @ht.to_a
62 | end
63 |
64 | def to_s
65 | @ht.to_s
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/hypertext.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | #
3 | # Copyright (c) 2021 Michel Martens
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.
22 |
23 | class Hypertext
24 | ENTITIES = {
25 | "'" => ''',
26 | '&' => '&',
27 | '"' => '"',
28 | '<' => '<',
29 | '>' => '>',
30 | }
31 |
32 | def self.render(array, indent = " ", level = 0)
33 | indentation = indent * level
34 |
35 | array.map do |element|
36 | if Array === element
37 | render(element, indent, level + 1)
38 | else
39 | sprintf "%s%s\n", indentation, element
40 | end
41 | end.join
42 | end
43 |
44 | def self.escape(str)
45 | str.gsub(/['&\"<>]/, ENTITIES)
46 | end
47 |
48 | def initialize
49 | @dom = []
50 |
51 | if block_given?
52 | yield self
53 | end
54 | end
55 |
56 | def append(value)
57 | @dom.push(*value)
58 | end
59 |
60 | def tag(name, attributes = {})
61 | atts = compile(attributes)
62 |
63 | if block_given?
64 | append("<#{name}#{atts}>")
65 |
66 | original, @dom = @dom, []
67 | yield
68 | @dom = original << @dom
69 |
70 | append("#{name}>")
71 | else
72 | append("<#{name}#{atts} />")
73 | end
74 | end
75 |
76 | def text(value)
77 | append(escape(value))
78 | end
79 |
80 | def to_a
81 | @dom
82 | end
83 |
84 | def to_s(indent = " ")
85 | self.class.render(@dom, indent)
86 | end
87 |
88 | private
89 |
90 | def escape(str)
91 | self.class.escape(str)
92 | end
93 |
94 | def compile(attributes)
95 | attributes.map do |key, val|
96 | case val
97 | when false
98 | when true
99 | %[ #{key}]
100 | else
101 | %[ #{key}="#{escape(val.to_s)}"]
102 | end
103 | end.join
104 | end
105 | end
106 |
--------------------------------------------------------------------------------