GithubHelp home page GithubHelp logo

schneiderfelipe / htm.jl Goto Github PK

View Code? Open in Web Editor NEW
4.0 1.0 0.0 246 KB

๐Ÿ HTM.jl: Hyperscript Tagged Markup in Julia.

Home Page: https://schneiderfelipe.github.io/HTM.jl/

License: MIT License

Julia 100.00%
julia macro svg html interpolation htm jsx hyperscript lit-html dsl

htm.jl's Introduction

WARNING: this project has been retired. For an excellent alternative, see MechanicalRabbit/HypertextLiteral.jl.

๐Ÿ HTM.jl (Hyperscript Tagged Markup in Julia)

Dev Build Status Coverage

julia> using HTM

julia> box(content) = htm"<div id=$(rand(1:100))>$(content)</div>"
box (generic function with 1 method)

julia> b = box("Hello HTM.jl๐Ÿ!")
<div id="23">Hello HTM.jl๐Ÿ&#33;</div>

HTM.jl is JSX-like syntax for Julia. It is backend-agnostic but uses Hyperscript.jl for generating HTML elements by default:

julia> dump(b, maxdepth=1)
Hyperscript.Node{Hyperscript.HTMLSVG}
  context: Hyperscript.HTMLSVG
  tag: String "div"
  children: Array{Any}((1,))
  attrs: Dict{String, Any}

(One of the advantages of using Hyperscript.jl is that objects are lazily rendered.)

And since @htm_str is a macro, parsing happens at compile time:

julia> @macroexpand htm"<div id=$(rand(1:100))>$(content)</div>"
:(create_element("div", process(Dict(("id" => rand(1:100),))), (content,)))

Syntax

The syntax was inspired by JSX, lit-html, htm, and Hypertext Literal:

  • Spread attributes: htm"<div $(attrs)></div>"
  • Self-closing tags: htm"<div />"
  • Multiple root elements (fragments): htm"<div /><div />"
  • Short circuit rendering: htm"<div>$(hidefruit || '๐Ÿ')</div>"
  • Boolean attributes: htm"<div draggable />" or htm"<div draggable=$(true) />"
  • HTML optional quotes: htm"<div class=fruit></div>"
  • Styles: htm"<div style=$(style)></div>"
  • Universal end-tags: htm"<div>๐Ÿ<//>"
  • HTML-style comments: htm"<div><!-- ๐ŸŒ --></div>"

Furthermore, the components can be constructed using Julia's display system: htm"$(Fruit(\"pineapple\", '๐Ÿ'))".

Installation

HTM.jl can be installed using the Julia package manager. From the Julia REPL, type ] to enter the Pkg REPL mode and run:

pkg> add https://github.com/schneiderfelipe/HTM.jl

Usage

See the documentation (I hope you like pineapples).

Project status

HTM.jl is a small (<400 lines of code) open-source Julia project. It was once called JSX.jl. Its main goal is to create a fully-featured, backend-agnostic (any library that produces HTML elements can be used as a backend) alternative to the @html_str macro. I also wanted @md_str-like string interpolations, JSX-like syntax, and full compatibility with Julia objects through Julia's display system.

HTM.jl is a work in progress but is already quite usable and fast. It may not be ready for production use yet though. Please help me improve it by sharing your feedback. ๐Ÿ™

htm.jl's People

Contributors

github-actions[bot] avatar schneiderfelipe avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

htm.jl's Issues

Rendering multiple components

julia> using JSX

julia> numbers = [1, 2, 3, 4, 5];

julia> list = map(x -> htm"<li>$x</li>", numbers)
5-element Array{JSX.Node{:li},1}:
 JSX.Node{:li}(Any[1], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[2], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[3], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[4], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[5], Pair{Symbol,Any}[])

julia> htm"<ul>$list</ul>"  # Should be <ul><li>1</li><li>2</li>...</ul>!
<ul>5-element Array{JSX.Node{:li},1}:
 JSX.Node{:li}(Any[1], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[2], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[3], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[4], Pair{Symbol,Any}[])
 JSX.Node{:li}(Any[5], Pair{Symbol,Any}[])</ul>

This is OK with JSX.

Children in components

That would allow for powerful composition.

julia> using JSX

julia> function fancyborder(children; color="red")  # BUG: class=$(... throws an error due to a space char!
           return htm"""
               <div class=$("fancyborder fancyborder-" * color)>
                   $children
               </div>
           """
       end
fancyborder (generic function with 1 method)

julia> function welcomedialog()
           return htm"""
               <fancyborder color="blue">
                   <h1 class="dialog-title">
                       Welcome
                   </h1>
                   <p class="dialog-message">
                       Thank you for visiting our spacecraft!
                   </p>
               </fancyborder>
           """
       end
welcomedialog (generic function with 1 method)

Closely related to #4. See also here.

Support htm"<div $(props...)/>" by bypassing merge

This means supporting pairs:

julia> htm"<div $(props)/>"
<div href="google.com" title="main"></div>

julia> htm"<div $(props...)/>"
ERROR: MethodError: no method matching merge(::Pair{String, String}, ::Pair{String, String})
Closest candidates are:
  merge(::NamedTuple, ::Any) at namedtuple.jl:277
Stacktrace:
 [1] top-level scope
   @ REPL[35]:1

(Related: #20.)

ERROR: LoadError: Tag cannot be empty.

A last interpolated prop without parenthesis throws an error:

julia> using HyperscriptLiteral

julia> width, height = 100, 100
(100, 100)

julia> htm"<svg width=$(width) height=$(height)>
         $(htm\"<circle
                   cx=50
                   cy=50
                   r=40
                   fill=red
                ></circle>\")
       </svg>"
<svg height="100" width="100">
  <circle cy="50" r="40" fill="red" cx="50" />
</svg>

julia> htm"<svg width=$(width) height=$height>
         $(htm\"<circle
                   cx=50
                   cy=50
                   r=40
                   fill=red
                ></circle>\")
       </svg>"
ERROR: LoadError: Tag cannot be empty.
Stacktrace:
  [1] error(s::String)
    @ Base ./error.jl:33
  [2] validatetag
    @ ~/.julia/packages/Hyperscript/IZSb2/src/Hyperscript.jl:409 [inlined]
  [3] Hyperscript.Node(ctx::Hyperscript.HTMLSVG, tag::String, children::Tuple{}, attrs::Dict{String, Any})
    @ Hyperscript ~/.julia/packages/Hyperscript/IZSb2/src/Hyperscript.jl:87
  [4] create_element
    @ ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:49 [inlined]
  [5] parsetag(io::IOBuffer; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:108
  [6] parseelem(io::IOBuffer; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:95
  [7] parseelems!(predicate::HyperscriptLiteral.var"#12#13"{String}, io::IOBuffer, elems::Vector{Any}; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:83
  [8] #parseelems#6
    @ ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:78 [inlined]
  [9] parsetag(io::IOBuffer; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:111
 [10] parseelem(io::IOBuffer; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:95
 [11] parseelems!(predicate::HyperscriptLiteral.var"#4#5", io::IOBuffer, elems::Vector{Any}; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:83
 [12] #parseelems#6
    @ ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:78 [inlined]
 [13] #parseelems#3
    @ ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:75 [inlined]
 [14] parse(io::IOBuffer; interp::Bool)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:58
 [15] #parse#2
    @ ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:63 [inlined]
 [16] var"@htm_str"(__source__::LineNumberNode, __module__::Module, html::Any)
    @ HyperscriptLiteral ~/Dropbox/HyperscriptLiteral.jl/src/HyperscriptLiteral.jl:11
in expression starting at REPL[6]:1

Backslashes in attribute

julia> using HTM

julia> htm"<link rel=stylesheet href='css/normalize.css' />"
<link rel="stylesheet" href="css/normalize.css" />

julia> htm"<link rel=stylesheet href=css/normalize.css />"
2-element Vector{Any}:
 <link rel="stylesheet" href="css" />
 "normalize.css />"

Escape based on tag

julia> using HTM

julia> htm"""
       <script>
           function f(x) {
               return x + 1;
           }
       </script>
       """
<script>
    function f&#40;x&#41; &#123;
        return x &#43; 1;
    &#125;
</script>

julia> htm"""
       <style>
           p {
               color: blue;
           }
       </style>
       """
<style>
    p &#123;
        color: blue;
    &#125;
</style>

This requires a small change in the default create_element.

Should join classes if vector?

julia> using HyperscriptLiteral

julia> classes = ["one", "two"];

julia> htm"<div class=$(classes) />"  # should be
<div class="one two" />

Currently, the output is

julia> htm"<div class=$(classes) />"
<div class="onetwo"></div>

Passing HTML entities

julia> using HTM

julia> htm"<div>&nbsp;</div>"  # Not what you want
<div>&#38;nbsp;</div>

julia> htm"<div>$(HTML(\"&nbsp;\"))</div>"  # What you want
<div>&nbsp;</div>

This belongs to the docs.

Preventing component from rendering

Prevent rendering of a component by returning nothing:

julia> using JSX

julia> function warningbanner(; warn=true)
           if !warn
               return nothing
           end
           return htm"""
               <div class="warning">
                   Warning!
               </div>
           """
       end
warningbanner (generic function with 1 method)

julia> function app(; warn=false)
           return htm"""
               <div>
                   <warningbanner warn=$warn />
               </div>
           """
       end
app (generic function with 1 method)

julia> app(warn=true)
<div><div class="warning"> Warning&#33; </div></div>

julia> app(warn=false)  # Should give <div></div>!
<div>nothing</div>

This is supported by JSX.

What we want from htm

Non-dictionary spread props

This works:

julia> using HTM

julia> props = Dict(
           "onmouseover" => "this.style.transform = 'rotate(5deg)'",
           "onmousedown" => "this.style.transform = 'rotate(25deg)'",
           "onmouseup" => "this.style.transform = 'rotate(5deg)'",
           "onmouseout" => "this.style.transform = ''",
           "onclick" => "alert('You clicked! ๐ŸŽ‰')",
       );

julia> htm"<p><button $(props)>Click me!</button></p>"
<p><button onclick="alert('You clicked! ๐ŸŽ‰')" onmouseout="this.style.transform = ''" onmouseup="this.style.transform = 'rotate(5deg)'" onmousedown="this.style.transform = 'rotate(25deg)'" onmouseover="this.style.transform = 'rotate(5deg)'">Click me&#33;</button></p>

This does not:

julia> props = [
           "onmouseover" => "this.style.transform = 'rotate(5deg)'",
           "onmousedown" => "this.style.transform = 'rotate(25deg)'",
           "onmouseup" => "this.style.transform = 'rotate(5deg)'",
           "onmouseout" => "this.style.transform = ''",
           "onclick" => "alert('You clicked! ๐ŸŽ‰')",
       ];

julia> htm"<p><button $(props)>Click me!</button></p>"
ERROR: MethodError: no method matching merge(::Vector{Pair{String, String}})
Closest candidates are:
  merge(::AbstractDict, ::AbstractDict...) at abstractdict.jl:311
  merge(::Union{Function, Type}, ::AbstractDict, ::AbstractDict...) at abstractdict.jl:356
  merge(::NamedTuple{(), T} where T<:Tuple, ::NamedTuple{(), T} where T<:Tuple) at namedtuple.jl:256
  ...
Stacktrace:
 [1] top-level scope
   @ REPL[22]:1

(Related: #18.)

Interpolate styles

julia> using HyperscriptLiteral

julia> styles = Dict("font-size" => "25px", "padding-left" => "2em")

julia> htm"<div style=$(styles) />"  # should be
<div style="font-size: 25px; padding-left: 2em;" />

Currently,

julia> htm"<div style=$(styles) />"
<div style="Dict(&#34;font-size&#34; =&#62; &#34;25px&#34;, &#34;padding-left&#34; =&#62; &#34;2em&#34;)"></div>

The component concept might not be Julian enough

Since we can do

using JSX

struct Dialog{T}
    message::T
end
Base.show(io, mime::MIME"text/html", d::Dialog) = show(
    io,
    mime,
    htm"""<span class="message">$(d.message)</span>""",
)

htm"""<h1>$(Dialog("A message in a bottle"))</h1>"""

we don't need (traditional) components.

This is related to #1, #2, #4 and #5.

Generic closing tag

using HTM

el = "h1"
user = "Felipe"
h"<$(el)>Hello, $(user)!</>"
# => <h1>Hello, Felipe!<h1>

All things SVG

I would like to make it easier to write SVG from scratch:

  • Optional quotes: htm"<rect width=300 height=100 />" (#2)
  • CSS styles for SVG elements: htm"<circle r=50 style=$(style) />" (see #10, #21 and #25)
  • Interpolate points attribute: htm"<polygon points=$(points) />"
  • SVG <path> as data:
    • d: htm"<path d=$(instructions) />"
    • stroke-dasharray: htm"<path stroke-dasharray=$(dasharray) />"

Other libraries could pick up from there:

Interpolation in keys

julia> htm"<$(\"div\") class=$(\"active\") disabled>$(\"Hello world!\")</$(\"div\")>"  # OK
<div class="active" disabled>Hello world&#33;</div>

julia> htm"<$(\"div\") class=$(\"active\") $(\"disabled\")>$(\"Hello world!\")</$(\"div\")>"
ERROR: MethodError: no method matching merge(::OrderedCollections.LittleDict{Any, Any, Vector{Any}, Vector{Any}}, ::String)
Closest candidates are:
  merge(::OrderedCollections.LittleDict, ::AbstractDict) at /home/schneider/.julia/packages/OrderedCollections/cP9uu/src/little_dict.jl:159
  merge(::AbstractDict, ::AbstractDict...) at abstractdict.jl:311
  merge(::NamedTuple, ::Any) at namedtuple.jl:277
Stacktrace:
 [1] top-level scope
   @ REPL[12]:1

julia> htm"<$(\"div\") $(\"class\")=$(\"active\") disabled>$(\"Hello world!\")</$(\"div\")>"
ERROR: MethodError: no method matching merge(::OrderedCollections.LittleDict{Any, Any, Vector{Any}, Vector{Any}}, ::String)
Closest candidates are:
  merge(::OrderedCollections.LittleDict, ::AbstractDict) at /home/schneider/.julia/packages/OrderedCollections/cP9uu/src/little_dict.jl:159
  merge(::AbstractDict, ::AbstractDict...) at abstractdict.jl:311
  merge(::NamedTuple, ::Any) at namedtuple.jl:277
Stacktrace:
 [1] top-level scope
   @ REPL[13]:1

julia> htm"<$(\"div\") $(\"class\")=$(\"active\") $(\"disabled\")>$(\"Hello world!\")</$(\"div\")>"
ERROR: MethodError: no method matching merge(::OrderedCollections.LittleDict{Any, Any, Vector{Any}, Vector{Any}}, ::String, ::String)
Closest candidates are:
  merge(::OrderedCollections.LittleDict, ::AbstractDict) at /home/schneider/.julia/packages/OrderedCollections/cP9uu/src/little_dict.jl:159
  merge(::AbstractDict, ::AbstractDict...) at abstractdict.jl:311
  merge(::NamedTuple, ::Any) at namedtuple.jl:277
Stacktrace:
 [1] top-level scope
   @ REPL[14]:1

Ignore booleans?

This is done by JSX and can provide neat control flow:

julia> using HTM

julia> htm"<div>$(true && \"Hi\")</div>"
<div>Hi</div>

julia> htm"<div>$(false && \"Hi\")</div>"  # If we ignore booleans like JSX, this becomes <div></div>
<div>false</div>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.