From 49a00441195bf1b7ae88610731a3e557cb96469d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Mon, 11 Dec 2023 20:08:35 +0100 Subject: [PATCH] vue parsing: support flexgrid kwargs for row, column and cell; add context --- src/StippleUIParser.jl | 198 ++++++++++++++++++++++++++++++++++++----- test/runtests.jl | 4 +- 2 files changed, 176 insertions(+), 26 deletions(-) diff --git a/src/StippleUIParser.jl b/src/StippleUIParser.jl index 52ef8120..12b1594d 100644 --- a/src/StippleUIParser.jl +++ b/src/StippleUIParser.jl @@ -106,23 +106,29 @@ end k => v end - -function function_parser(tag, attrs, context = @__MODULE__) + +function function_parser(tag, attrs; context = @__MODULE__) tag_str = replace(String(typeof(tag).parameters[1]), "-" => "__") julia_str = String(typeof(tag).parameters[1]) julia_fn = Symbol(replace(startswith(julia_str, "q-") ? julia_str[3:end] : julia_str, "-" => "")) - M = if isdefined(Stipple, julia_fn) - Stipple - elseif isdefined(StippleUI, julia_fn) + M = if isdefined(StippleUI, julia_fn) StippleUI + elseif isdefined(Stipple, julia_fn) + Stipple elseif isdefined(context, julia_fn) context else nothing end + # if tag_str == "q__layout" + # julia_str = Symbol("StippleUI.layout") + # M = StippleUI + # end + arg_str = "" is_html_tag = false + f = nothing if M !== nothing && tag_str != "q__input" f = getfield(M, julia_fn) attr_names = rstrip.(keys(attrs), '!') @@ -140,6 +146,34 @@ function function_parser(tag, attrs, context = @__MODULE__) end end + # parse flexgrid attributes + if !is_html_tag || tag_str in ("div", "col", "row", "cell") + kk = String.(collect(keys(attrs))) + pos = findfirst(startswith(r":?class$"), kk) + if pos !== nothing + k = kk[pos] + v = attrs[k] + v_new, removed = remove_class(v, r"^col(-..)?(-(\d+|auto))?$") + + if ! isempty(removed) + if v_new in ("", "''", "R\"''\"") + delete!(attrs, k) + else + attrs[k] = v_new + end + for r in removed + xx = split(r, '-') + (length(xx) == 1 || length(xx) == 2 && xx[2][1] ∈ 'a':'z' ) && push!(xx, "0") + if length(xx) == 2 + attrs[:col] = xx[2] + else + attrs[Symbol(xx[2])] = xx[3] + end + end + end + end + end + name_dict = LittleDict(zip(attr_names, keys(attrs))) # if variable accepts Symbols but no Strings convert to Symbol arg_str = join([!(String <: type_dict[k]) && Symbol <: type_dict[k] ? symrepr(attrs[name_dict[k]]) : attrs[name_dict[k]] for k in args], ", ") @@ -153,7 +187,7 @@ function function_parser(tag, attrs, context = @__MODULE__) index = startswith.(values(attrs), '"') if ! all(index) attrs = LittleDict(startswith(v, '"') ? k => v : - (endswith(k, '"') ? string(k[1:end-1], "!\"") : string(k, '!')) => rawrepr(startswith(v, 'R') ? v[3:end-1] : v[2:end]) + (endswith("$k", '"') ? string(k[1:end-1], "!\"") : string(k, '!')) => rawrepr(startswith(v, 'R') ? v[3:end-1] : v[2:end]) for (k, v) in attrs ) end @@ -163,7 +197,20 @@ function function_parser(tag, attrs, context = @__MODULE__) fn_str = if M === nothing || tag_str == "q__input" startswith(tag_str, "q__") ? "quasar(:$(tag_str[4:end]), " : startswith(tag_str, "vue__") ? "vue(:$(tag_str[6:end]), " : "xelem(:$tag_str, " else - julia_fn == :div && (julia_fn = :htmldiv) + if julia_fn == :div + julia_fn = :htmldiv + elseif M == Stipple + # if a function is defined in both Stipple and context and if they are not identical, prefix it with `Stipple.` + if isdefined(context, julia_fn) && getfield(context, julia_fn) != f + julia_fn = Symbol("Stipple.$julia_fn") + end + elseif M == StippleUI + # if a function is defined in both StippleUI and Stipple or context and if they are not identical, prefix it with `StippleUI.` + if isdefined(Stipple, julia_fn) && getfield(Stipple, julia_fn) != f || + isdefined(context, julia_fn) && getfield(context, julia_fn) != f + julia_fn = Symbol("StippleUI.$julia_fn") + end + end "$julia_fn(" end @@ -178,7 +225,7 @@ function attr_to_paramstring(attr::Pair) "$(attr[1])=\"$(attr[2])\"" end -function node_to_stipple(el::EzXML.Node, level = 0; @nospecialize(indent::Union{Int, String} = 4), pre::Bool = false, vec_sep::String = ",\n") +function node_to_stipple(el::EzXML.Node, level = 0; @nospecialize(indent::Union{Int, String} = 4), pre::Bool = false, vec_sep::String = ",\n", context = @__MODULE__) startlevel = level level < 0 && (level = 0) @@ -205,11 +252,11 @@ function node_to_stipple(el::EzXML.Node, level = 0; @nospecialize(indent::Union{ arg_str = "" attrs = attr_dict(stipple_attr, el) - fn_str, arg_str, new_attrs = function_parser(Val(Symbol(el.name)), attrs) + fn_str, arg_str, new_attrs = function_parser(Val(Symbol(el.name)), attrs; context) attr_str = join(attr_to_kwargstring.(collect(new_attrs)), ", ") - children = node_to_stipple.(nodes(el), startlevel + 1; indent, pre, vec_sep) + children = node_to_stipple.(nodes(el), startlevel + 1; indent, pre, vec_sep, context) children = children[length.(children) .> 0] children_str = join(children, vec_sep) @@ -283,14 +330,15 @@ function node_to_html(el::EzXML.Node, level = 0; @nospecialize(indent::Union{Int end """ - parse_vue_html(html, level = 0; indent::Union{String, Int} = 4, vec_sep::String = ",\n") + parse_vue_html(html, level = 0; indent::Union{String, Int} = 4, vec_sep::String = ",\n", context = @__MODULE__) Parse html code to Julia/StippleUI code with automatic line breaks and indenting. Indenting can be determined by - `level: starting level for formatting; negative values are allowed, negative levels are not indented - `indent`: either Integer for number of ' ' characters per level or a string value - `vec_sep`: separator in array listings, reasonable values are `",\\n"`, `"\\n\\n"`, `",\\n\\n"` +- `context`: context for evaluation """ -function parse_vue_html(html; level::Integer = 0, indent::Union{String, Int} = 4, vec_sep::String = ",\n") +function parse_vue_html(html; level::Integer = 0, indent::Union{String, Int} = 4, vec_sep::String = ",\n", context = @__MODULE__) startlevel = level level < 0 && (level = 0) @@ -320,12 +368,12 @@ function parse_vue_html(html; level::Integer = 0, indent::Union{String, Int} = 4 # remove the html -> body levels if root == :html - node_to_stipple(root_node, startlevel; indent, vec_sep) + node_to_stipple(root_node, startlevel; indent, vec_sep, context) else # remove the html / body levels children = nodes(root == :no_root ? root_node.firstelement : root_node) is_single = length(children) <= 1 - children_str = node_to_stipple.(children, is_single ? startlevel : startlevel + 1; indent, vec_sep) + children_str = node_to_stipple.(children, is_single ? startlevel : startlevel + 1; indent, vec_sep, context) replace(is_single ? children_str[1] : "$indent_str[\n$(join(filter(!isempty, children_str), vec_sep))$indent_str\n]", AT_MASK => "@") end |> ParsedHTMLString end @@ -376,11 +424,11 @@ prettify(doc::EzXML.Document; level::Int = 0, indent::Union{String, Int} = 4) = prettify(v::Vector; level::Int = 0, indent::Union{String, Int} = 4) = prettify(join(v); level, indent) -function function_parser(tag::Val{Symbol("q-input")}, attrs, context = @__MODULE__) +function function_parser(tag::Val{Symbol("q-input")}, attrs; context = @__MODULE__) kk = String.(collect(keys(attrs))) pos = findfirst(startswith(r"fieldname$|var\"v-model."), kk) if pos === nothing - function_parser(Val(:q__input), attrs) + function_parser(Val(:q__input), attrs; context) else haskey(attrs, "label") || (attrs["label"] = "\"\"") k = kk[pos] @@ -389,32 +437,134 @@ function function_parser(tag::Val{Symbol("q-input")}, attrs, context = @__MODULE attrs["fieldname"] = v if k == "var\"v-model.number\"" - function_parser(Val(:numberfield), attrs) + function_parser(Val(:numberfield), attrs; context) else - function_parser(Val(:textfield), attrs) + function_parser(Val(:textfield), attrs; context) + end + end +end + +function contains_class(class::String, subclass::Union{String, Regex}) + # removes any quotation ("<...>", R"<...>") and stores the captured parts for concatentation at the end + m = match(r"^(R?\"+)?(.*?)(\"+)?$", class) + class = m.captures[2] + isreactive = m.captures[1] !== nothing && contains(m.captures[1], "R") + + classes = isreactive ? split(class, r"\s*\+\s*") : [class] + for (n, class) in enumerate(classes) + isreactive && first(class) != ''' && continue + # only evaluate explicit terms when they are at the beginning or when their first character is a whitespace + n > 1 && length(class) > 1 && !contains(class[2:2], r"\s") && continue + cc = split(strip(class, ''')) + for c in cc + (subclass isa String ? subclass == c : contains(c, subclass)) && return true + end + end + false +end + +function remove_class(class::String, subclass::Union{String, Regex}) + removed = String[] + # removes any quotation ("<...>", R"<...>") and stores the captured parts for concatentation at the end + m = match(r"^(R?\"+)?(.*?)(\"+)?$", class) + class = m.captures[2] + isquoted = m.captures[1] !== nothing + + isreactive = m.captures[1] !== nothing && contains(m.captures[1], "R") + classes = isreactive ? split(class, r"\s*\+\s*") : [class] + for n in length(classes):-1:1 + class = classes[n] + isreactive && first(class) != ''' && continue + # only evaluate explicit terms when they are at the beginning or when their first character is a whitespace + n > 1 && length(class) > 1 && !contains(class[2:2], r"\s") && continue + cc = split(strip(class, ''')) + remove_outer = false + for i in length(cc):-1:1 + remove = false + c = cc[i] + if subclass isa String ? subclass == c : contains(c, subclass) + pushfirst!(removed, c) # because of inverted + deleteat!(cc, i) + remove_outer = remove = true + end + end + + if remove_outer + classes[n] = join(cc, ' ') + first(class) == ''' && (classes[n] = "' " * classes[n] * ''') + end + classes[n] in ("", "''", "' '") && deleteat!(classes, n) + end + + c = join(classes, " + ") + c == "" && isreactive && (c = "''") + + if isquoted + c = m.captures[1] * c * m.captures[3] + if c[1] == 'R' + x = repr(Symbol(c[3:end-1])) + startswith(x, ":") && (c = x) + end + end + + return c, removed +end + +function function_parser(tag::Val{:div}, attrs, context = @__MODULE__) + kk = String.(collect(keys(attrs))) + pos = findfirst(startswith(r":?class$"), kk) + if pos === nothing + function_parser(Val(:htmldiv), attrs; context) + else + k = kk[pos] + v = attrs[k] + if k == "class" + for c in ("row", "column", r"^col(-..)?(-(\d+|auto))?$") + tagname = c isa Regex ? :cell : Symbol(c) + v_new, removed = remove_class(v, c) + isempty(removed) && continue + + if c isa Regex && ! isempty(removed) + # if there is at least one col element check if "st-col" is part of the class + v_new, removed = remove_class(v, "st-col") + isempty(removed) && continue + # remove "col" from the class + v_new, removed = remove_class(v_new, "col") + end + + if v_new[2:end-1] in ("", "''") + delete!(attrs, k) + else + attrs[k] = v_new + end + + return function_parser(Val(tagname), attrs; context) + end end + function_parser(Val(:htmldiv), attrs; context) end end """ - test_vue_parsing(html_string; prettify::Bool = true, level = 0, indent = 4) + test_vue_parsing(html_string; prettify::Bool = true, level = 0, indent = 4, context = @__MODULE__) Parse html code with automatic line breaks and indentingto Julia/StippleUI code, execute the code and prettify the result. Indenting can be determined by -- level: starting level for formatting; negative values are allowed, negative levels are not indented -- indent: either Integer for number of ' ' characters per level or a string value +- `level`: starting level for formatting; negative values are allowed, negative levels are not indented +- `indent`: either Integer for number of ' ' characters per level or a string value +- `context`: context for evaluation """ -function test_vue_parsing(html_string; prettify::Bool = true, level = 0, indent = 4) +function test_vue_parsing(html_string; prettify::Bool = true, level = 0, indent = 4, context = @__MODULE__) println("\nOriginal HTML string:") printstyled(html_string, "\n\n", color = :light_red) - julia_code = parse_vue_html(html_string; level, indent) + julia_code = parse_vue_html(html_string; level, indent, context) println("Julia code:") printstyled(julia_code, "\n\n", color = :blue) println("Produced HTML:") - new_html = eval(Meta.parse(julia_code)) + new_html = Core.eval(context, Meta.parse(julia_code)) printstyled(prettify ? StippleUIParser.prettify(new_html; level, indent) : new_html, "\n", color = :green) end diff --git a/test/runtests.jl b/test/runtests.jl index f0f71426..2423d79c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,7 +24,7 @@ end

Vowel count: {{vowels}} vowels.

""" - result = """Stipple.Html.div([ + result = """htmldiv([ textfield("", :message, ), br(), p( @@ -37,7 +37,7 @@ end @test parse_vue_html(doc_string) == result - result = """Stipple.Html.div([ + result = """htmldiv([ textfield("", :message, ) br()