phlex 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of phlex might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.editorconfig +5 -0
- data/.rubocop.yml +6 -0
- data/CONTRIBUTING.md +23 -0
- data/Rakefile +2 -2
- data/bench.rb +1 -1
- data/docs/assets/application.css +2 -0
- data/docs/build.rb +4 -4
- data/docs/components/callout.rb +5 -5
- data/docs/components/code_block.rb +18 -18
- data/docs/components/example.rb +23 -23
- data/docs/components/heading.rb +5 -5
- data/docs/components/layout.rb +42 -41
- data/docs/components/markdown.rb +19 -19
- data/docs/components/tabs/tab.rb +18 -18
- data/docs/components/tabs.rb +21 -21
- data/docs/components/title.rb +5 -5
- data/docs/page_builder.rb +32 -32
- data/docs/pages/application_page.rb +3 -3
- data/docs/pages/index.rb +23 -25
- data/docs/pages/rails_integration.rb +58 -0
- data/docs/pages/templates.rb +238 -238
- data/docs/pages/views.rb +175 -0
- data/fixtures/compilation/vcall.rb +38 -0
- data/fixtures/dummy/app/views/articles/form.rb +9 -9
- data/fixtures/dummy/app/views/card.rb +8 -8
- data/fixtures/dummy/app/views/heading.rb +5 -5
- data/fixtures/dummy/config/routes.rb +1 -1
- data/fixtures/dummy/db/schema.rb +2 -2
- data/fixtures/layout.rb +24 -24
- data/fixtures/page.rb +34 -34
- data/fixtures/test_helper.rb +2 -2
- data/fixtures/view_helper.rb +16 -0
- data/lib/generators/phlex/component/USAGE +1 -1
- data/lib/generators/phlex/component/component_generator.rb +8 -8
- data/lib/generators/phlex/component/templates/{component.rb.erb → view.rb.erb} +1 -1
- data/lib/install/phlex.rb +18 -0
- data/lib/overrides/symbol/name.rb +1 -1
- data/lib/phlex/block.rb +12 -12
- data/lib/phlex/buffered.rb +13 -13
- data/lib/phlex/compiler/formatter.rb +91 -0
- data/lib/phlex/compiler/generators/standard_element.rb +30 -0
- data/lib/phlex/compiler/generators/void_element.rb +29 -0
- data/lib/phlex/compiler/optimizers/base_optimizer.rb +34 -0
- data/lib/phlex/compiler/optimizers/vcall.rb +29 -0
- data/lib/phlex/compiler/visitors/base_visitor.rb +19 -0
- data/lib/phlex/compiler/visitors/component.rb +28 -0
- data/lib/phlex/compiler/visitors/component_method.rb +28 -0
- data/lib/phlex/compiler/visitors/file.rb +17 -0
- data/lib/phlex/compiler.rb +50 -0
- data/lib/phlex/configuration.rb +3 -3
- data/lib/phlex/engine.rb +11 -0
- data/lib/phlex/html.rb +128 -17
- data/lib/phlex/rails/tag_helpers.rb +23 -23
- data/lib/phlex/renderable.rb +32 -32
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex/view.rb +223 -0
- data/lib/phlex.rb +27 -12
- data/lib/tasks/phlex_tasks.rake +10 -0
- metadata +39 -10
- data/CHANGELOG.md +0 -5
- data/docs/pages/components.rb +0 -175
- data/fixtures/component_helper.rb +0 -16
- data/lib/phlex/component.rb +0 -196
- data/lib/phlex/rails.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4314dc80b5da3795454b1906f8c51b0b875fc6fea6962c2a77476dd6a4f01d34
|
4
|
+
data.tar.gz: 7ca6921fed9c3d2d6122dd0e77e2a97f5f100dc4a003d76c76c8dd407fddf86e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e7a837a1c2584e7c7f0c7955c2f49f48d4cbc049e19ba2cb707f6cfb3662edca2c05dc5cb80fc59ec6d50416fcc60894ec77bf3cc987cf22aab4ea9c470e95a
|
7
|
+
data.tar.gz: 1365cfd00e035b4364a66d89d3657e1d49745e1950d64b7b0fe9a17bcf27551d59447f6cd86451c3279886303cef119a69b8c752acddc81045b0eefc66fd5a4f
|
data/.editorconfig
ADDED
data/.rubocop.yml
CHANGED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Contributing to Phlex
|
2
|
+
|
3
|
+
## Priorities
|
4
|
+
|
5
|
+
We’re trying to provide the best possible developer experience for the people using Phlex in their apps and the best possible performance for the users of those apps.
|
6
|
+
|
7
|
+
Phlex is incredibly complex and requires a lot of meta-programming but when you use it, it feels simple. Phlex views feel like plain old Ruby objects. You just subclass and define a couple of methods. That’s it. That’s how it should be.
|
8
|
+
|
9
|
+
## Setup
|
10
|
+
|
11
|
+
- Install dependencies `bundle install`
|
12
|
+
- Run the tests `bundle exec sus`
|
13
|
+
- Run Rubocop and auto-correct `bundle exec rubocop -A`
|
14
|
+
|
15
|
+
## Tests
|
16
|
+
|
17
|
+
We use the **[Sus](https://github.com/ioquatix/sus)** testing framework. It feels a bit like RSpec with a few small differences. There’s no documentation for Sus at the moment, but you should be able to pick up the basics from reading other tests or looking at the implementation in the Sus repo.
|
18
|
+
|
19
|
+
You can run all the tests with `bundle exec sus`.
|
20
|
+
|
21
|
+
## Documentation
|
22
|
+
|
23
|
+
Documentation is deployed when it’s merged into the `latest` branch with a release. But you can build and preview the docs locally by running `bin/docs`.
|
data/Rakefile
CHANGED
data/bench.rb
CHANGED
data/docs/assets/application.css
CHANGED
data/docs/build.rb
CHANGED
@@ -8,10 +8,10 @@ require "fileutils"
|
|
8
8
|
Bundler.require :docs
|
9
9
|
|
10
10
|
Zeitwerk::Loader.new.tap do |loader|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
loader.push_dir(__dir__)
|
12
|
+
loader.ignore(__FILE__)
|
13
|
+
loader.setup
|
14
|
+
loader.eager_load
|
15
15
|
end
|
16
16
|
|
17
17
|
FileUtils.mkdir_p("#{__dir__}/dist")
|
data/docs/components/callout.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
class Callout < Phlex::View
|
5
|
+
def template(&block)
|
6
|
+
div(class: "rounded bg-orange-50 text-sm p-5 border border-orange-100", &block)
|
7
|
+
end
|
8
|
+
end
|
9
9
|
end
|
@@ -1,26 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
4
|
+
class CodeBlock < Phlex::View
|
5
|
+
FORMATTER = Rouge::Formatters::HTML.new
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def initialize(code, syntax:)
|
8
|
+
@code = code
|
9
|
+
@syntax = syntax
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
def template
|
13
|
+
pre(class: "highlight p-5 whitespace-pre-wrap bg-stone-50") {
|
14
|
+
raw FORMATTER.format(
|
15
|
+
lexer.lex(@code)
|
16
|
+
)
|
17
|
+
}
|
18
|
+
end
|
19
19
|
|
20
|
-
|
20
|
+
private
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def lexer
|
23
|
+
Rouge::Lexer.find(@syntax)
|
24
|
+
end
|
25
|
+
end
|
26
26
|
end
|
data/docs/components/example.rb
CHANGED
@@ -1,32 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class Example < Phlex::View
|
5
|
+
def initialize
|
6
|
+
@sandbox = Module.new
|
7
|
+
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
def template(&block)
|
10
|
+
render Tabs.new do |t|
|
11
|
+
@t = t
|
12
|
+
yield self
|
13
|
+
end
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
def tab(name, code)
|
17
|
+
@t.tab(name) do
|
18
|
+
render CodeBlock.new(code, syntax: :ruby)
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
@sandbox.class_eval(code)
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
def execute(code)
|
25
|
+
output = @sandbox.class_eval(code)
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
@t.tab("HTML Output") do
|
28
|
+
render CodeBlock.new(HtmlBeautifier.beautify(output), syntax: :html)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
32
|
end
|
data/docs/components/heading.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
class Heading < Phlex::View
|
5
|
+
def template(&block)
|
6
|
+
h2(class: "text-xl font-bold mt-10 mb-5", &block)
|
7
|
+
end
|
8
|
+
end
|
9
9
|
end
|
data/docs/components/layout.rb
CHANGED
@@ -1,45 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
4
|
+
class Layout < Phlex::View
|
5
|
+
register_element :style
|
6
|
+
|
7
|
+
def initialize(title:)
|
8
|
+
@title = title
|
9
|
+
end
|
10
|
+
|
11
|
+
def template(&block)
|
12
|
+
doctype
|
13
|
+
|
14
|
+
html do
|
15
|
+
head do
|
16
|
+
meta charset: "utf-8"
|
17
|
+
title @title
|
18
|
+
link href: "/application.css", rel: "stylesheet"
|
19
|
+
style { raw Rouge::Theme.find("github").render(scope: ".highlight") }
|
20
|
+
end
|
21
|
+
|
22
|
+
body class: "p-12" do
|
23
|
+
div class: "max-w-screen-lg mx-auto grid grid-cols-4 gap-10" do
|
24
|
+
header class: "col-span-1" do
|
25
|
+
a(href: "/", class: "block") { img src: "/assets/logo.png", width: "150" }
|
26
|
+
|
27
|
+
nav do
|
28
|
+
ul do
|
29
|
+
li { a "Introduction", href: "/" }
|
30
|
+
li { a "Templates", href: "/templates" }
|
31
|
+
li { a "Views", href: "/views" }
|
32
|
+
li { a "Rails integration", href: "/rails-integration" }
|
33
|
+
li { a "Source code", href: "https://github.com/joeldrapper/phlex" }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
main(class: "col-span-3", &block)
|
39
|
+
|
40
|
+
footer class: "text-sm text-right col-span-4 py-10"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
45
46
|
end
|
data/docs/components/markdown.rb
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
4
|
+
class Markdown < Phlex::View
|
5
|
+
class Render < Redcarpet::Render::HTML
|
6
|
+
def header(text, level)
|
7
|
+
case level
|
8
|
+
when 1
|
9
|
+
Title.new.call { text }
|
10
|
+
else
|
11
|
+
Heading.new.call { text }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
15
|
|
16
|
-
|
16
|
+
MARKDOWN = Redcarpet::Markdown.new(Render.new, autolink: true, tables: true)
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
def initialize(content)
|
19
|
+
@content = content
|
20
|
+
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def template
|
23
|
+
raw MARKDOWN.render(@content)
|
24
|
+
end
|
25
|
+
end
|
26
26
|
end
|
data/docs/components/tabs/tab.rb
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
class Tabs
|
5
|
+
class Tab < Phlex::View
|
6
|
+
def initialize(name:, checked:)
|
7
|
+
@name = name
|
8
|
+
@checked = checked
|
9
|
+
end
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
def template(&block)
|
12
|
+
input class: "opacity-0 fixed peer", type: "radio", name: @_parent.unique_identifier, id: unique_identifier, checked: @checked
|
13
13
|
|
14
|
-
|
14
|
+
label @name, id: "#{unique_identifier}-label", for: unique_identifier, role: "tab", aria_controls: "#{unique_identifier}-panel", class: "order-1 py-2 px-5 bg-white text-sm border border-b-0 border-l-0 font-medium first-of-type:border-l first-of-type:rounded-tl last-of-type:rounded-tr before:absolute before:pointer-events-none before:w-full before:ring before:h-full before:left-0 before:top-0 before:hidden before:rounded peer-focus:before:block cursor-pointer"
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
div id: "#{unique_identifier}-panel", role: "tabpanel", aria_labelledby: "#{unique_identifier}-label", class: "tab hidden order-2 w-full border rounded-b rounded-tr overflow-hidden" do
|
17
|
+
@_parent.instance_exec(&block)
|
18
|
+
end
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
def unique_identifier
|
22
|
+
@unique_identifier ||= SecureRandom.hex
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
26
|
end
|
data/docs/components/tabs.rb
CHANGED
@@ -1,30 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class Tabs < Phlex::View
|
5
|
+
def initialize
|
6
|
+
@index = 1
|
7
|
+
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
def template(&block)
|
10
|
+
div class: "tabs flex flex-wrap relative my-5", role: "tablist" do
|
11
|
+
yield_content(&block)
|
12
|
+
end
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def tab(name, &block)
|
16
|
+
render(Tab.new(name: name, checked: first?), &block)
|
17
|
+
@index += 1
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def unique_identifier
|
21
|
+
@unique_identifier ||= SecureRandom.hex
|
22
|
+
end
|
23
23
|
|
24
|
-
|
24
|
+
private
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
def first?
|
27
|
+
@index == 1
|
28
|
+
end
|
29
|
+
end
|
30
30
|
end
|
data/docs/components/title.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Components
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
class Title < Phlex::View
|
5
|
+
def template(&block)
|
6
|
+
h1(class: "text-2xl font-bold my-5", &block)
|
7
|
+
end
|
8
|
+
end
|
9
9
|
end
|
data/docs/page_builder.rb
CHANGED
@@ -1,36 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class PageBuilder
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
4
|
+
ROOT = Pages::ApplicationPage
|
5
|
+
|
6
|
+
def self.build_all
|
7
|
+
ROOT.subclasses.each { |page| new(page).call }
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(page)
|
11
|
+
@page = page
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
FileUtils.mkdir_p(directory)
|
16
|
+
File.write(file, @page.new.call)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def file
|
22
|
+
"#{directory}/index.html"
|
23
|
+
end
|
24
|
+
|
25
|
+
def directory
|
26
|
+
if path == "index"
|
27
|
+
"#{__dir__}/dist"
|
28
|
+
else
|
29
|
+
"#{__dir__}/dist/#{path}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def path
|
34
|
+
@page.name.split("::")[1..].map { _1.gsub(/(.)([A-Z])/, '\1-\2') }.map(&:downcase).join("/")
|
35
|
+
end
|
36
36
|
end
|
data/docs/pages/index.rb
CHANGED
@@ -1,40 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Pages
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
class Index < ApplicationPage
|
5
|
+
def template
|
6
|
+
render Layout.new(title: "Introduction to Phlex") do
|
7
|
+
render Markdown.new(<<~MD)
|
8
|
+
# Introduction
|
9
9
|
|
10
|
-
|
10
|
+
Phlex is a framework for building fast, reusable, testable views in pure Ruby.
|
11
11
|
|
12
|
-
|
12
|
+
Each view object is an instance of a specific class of view. The nav-bar, for example, might contain three different nav-bar-items, but they’re all instances of the nav-bar-item class. This class, then, manifests everything there is to know about nav bar items in general. It models:
|
13
13
|
|
14
|
-
|
14
|
+
1. the **data** attributes being represented — perhaps url and label;
|
15
|
+
2. a **template**, which dictates how the data should be represented with HTML markup and CSS classes; and
|
16
|
+
3. **logic**, conditions, or calculations on the data — perhaps a predicate method to determine whether the link is active or not.
|
15
17
|
|
16
|
-
|
17
|
-
2. a **template**, which dictates how the data should be represented with HTML markup and CSS classes; and
|
18
|
-
3. **logic**, conditions, or calculations on the data — perhaps a predicate method to determine whether the link is active or not.
|
18
|
+
# Why use Phlex?
|
19
19
|
|
20
|
-
|
20
|
+
## Better developer experience 🎉
|
21
21
|
|
22
|
-
|
22
|
+
You don’t need to introduce a new language like Slim, HAML, or ERB. Phlex views are plain old Ruby objects: view classes are just Ruby classes, templates are just methods, and HTML tags are just method calls. If you know how to define a method that calls another method, you pretty much already know how to use Phlex.
|
23
23
|
|
24
|
-
|
24
|
+
## Better safety 🥽
|
25
|
+
Rails partials can implicitly depend on instance variables from a controller or view. This happens more often than you might think when copying code into a new partial extraction. If the partial is then rendered in a different context or the instance variable’s meaning changes, things can break quite severely without warning.
|
25
26
|
|
26
|
-
|
27
|
-
Rails partials can implicitly depend on instance variables from a controller or view. This happens more often than you might think when copying code into a new partial extraction. If the partial is then rendered in a different context or the instance variable’s meaning changes, things can break quite severely without warning.
|
27
|
+
Conversely, Phlex view templates render in an isolated execution context where only the instance variables and methods for the specific view are exposed.
|
28
28
|
|
29
|
-
|
29
|
+
## Better performance 🚀
|
30
30
|
|
31
|
-
|
31
|
+
Phlex is ~4.35× faster than ActionView and ~2× faster than ViewComponent. Phlex views are also streamable.
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
39
|
-
end
|
33
|
+
Rails apps typically spend 40-80% of the response time rendering views, so this could be a significant factor in overall app performance.
|
34
|
+
MD
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
40
38
|
end
|