├── 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 | #
67 | # 68 | # 69 | #
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("") 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 | --------------------------------------------------------------------------------