From 18e0407666ee9b7fb1b7b01b80b5107d1c571e3f Mon Sep 17 00:00:00 2001 From: Jesper Wendel Devantier Date: Mon, 20 Jan 2025 22:05:12 +0100 Subject: [PATCH] new release * nested components supported * syntax change * empty Lua lines (`%`) now permitted, can use to format code * docs: fix cross-referencing links bug * docs: update to reflect new syntax * docs: remove concept of directives, now there are components and Lua code blocks * add LICENSE file * add README.md file * api: htt.Component.is -- check if value is a component * api: all components now have a .name attribute containing the name they were defined with Defining components, before: ``` % @component foo ... % @end ``` Defining components, now: ``` % ... % ``` Lua code blocks, before: ``` % @code ... % @end ``` Lua code blocks, now: ``` %%% ... %%% ``` --- LICENSE | 9 + README.md | 20 +++ api/api_desc.lua | 67 +++++--- api/typestub.htt | 58 ++++--- docs/api_module.htt | 68 ++++---- docs/common.htt | 116 +++++++------ docs/common.htt.lua | 30 ++-- docs/debug.htt | 27 ++- docs/ex-go-ast.htt | 36 ++-- docs/examples/ast/ast1.htt | 4 +- docs/examples/ast/ast2.htt | 12 +- docs/examples/ast/ast3.htt | 16 +- docs/examples/ast/ast4.htt | 16 +- docs/examples/ast/ast5.htt | 24 +-- docs/examples/debug/err-compile.htt | 4 +- docs/examples/debug/err-exec.htt | 8 +- docs/examples/debug/err-stx.htt | 4 +- docs/examples/quick-start/components.htt | 88 +++++++--- docs/examples/syntax/def-component.txt | 4 +- docs/examples/syntax/examples.htt | 26 +-- docs/examples/syntax/grammar.txt | 15 +- docs/htt-intro.htt | 8 +- docs/htt.tmLanguage.json | 51 +++--- docs/model.lua | 2 +- docs/modules-and-files.htt | 32 ++-- docs/quick-start.htt | 92 ++++++---- docs/setup.htt | 16 +- docs/syntax-recap.htt | 72 ++++---- src/engine/tpl.zig | 20 ++- src/htt_typestubs.lua | 14 ++ src/prelude.lua | 18 ++ src/tpl/compiler.zig | 127 +++++++------- src/tpl/lexer.zig | 160 ++++++++---------- src/tpl/test_lexer.zig | 120 ++++++++++--- src/tpl/token.zig | 8 +- .../03_render/test_nested_components/outer1 | 5 + .../03_render/test_nested_components/outer2 | 5 + .../03_render/test_nested_components/outer3 | 5 + .../03_render/test_nested_components/outer4 | 5 + .../06_component_api/test_component_is/out | 6 + .../06_component_api/test_component_name/out | 5 + tests/tpl/inputs/01_render/hello.htt | 8 +- tests/tpl/inputs/02_basics/context.htt | 8 +- tests/tpl/inputs/02_basics/exprs.htt | 4 +- .../inputs/02_basics/line_continuation.htt | 8 +- tests/tpl/inputs/02_basics/lua_blocks.htt | 12 +- tests/tpl/inputs/02_basics/lua_lines.htt | 12 +- tests/tpl/inputs/02_basics/render_expr.htt | 8 +- .../tpl/inputs/02_basics/tpl_with_luafile.htt | 4 +- tests/tpl/inputs/03_render/ctx.htt | 16 +- tests/tpl/inputs/03_render/hoc.htt | 8 +- .../inputs/03_render/nested_components.htt | 47 +++++ .../03_render/test_nested_components.lua | 7 + .../tpl/inputs/04_formatting/indentation.htt | 40 ++--- .../04_formatting/inline_components.htt | 32 ++-- tests/tpl/inputs/04_formatting/nl_tests.htt | 36 ++-- .../tpl/inputs/05_require/foo/bar/nested.htt | 4 +- tests/tpl/inputs/05_require/same_dir.htt | 4 +- .../inputs/05_require/test_htt_require.lua | 2 - .../inputs/06_component_api/component_is.htt | 28 +++ .../06_component_api/component_name.htt | 25 +++ .../06_component_api/test_component_is.lua | 1 + .../06_component_api/test_component_name.lua | 1 + 63 files changed, 1042 insertions(+), 696 deletions(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 tests/tpl/expected/03_render/test_nested_components/outer1 create mode 100644 tests/tpl/expected/03_render/test_nested_components/outer2 create mode 100644 tests/tpl/expected/03_render/test_nested_components/outer3 create mode 100644 tests/tpl/expected/03_render/test_nested_components/outer4 create mode 100644 tests/tpl/expected/06_component_api/test_component_is/out create mode 100644 tests/tpl/expected/06_component_api/test_component_name/out create mode 100644 tests/tpl/inputs/03_render/nested_components.htt create mode 100644 tests/tpl/inputs/03_render/test_nested_components.lua create mode 100644 tests/tpl/inputs/06_component_api/component_is.htt create mode 100644 tests/tpl/inputs/06_component_api/component_name.htt create mode 100644 tests/tpl/inputs/06_component_api/test_component_is.lua create mode 100644 tests/tpl/inputs/06_component_api/test_component_name.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f35498c --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright 2025 Jesper Wendel Devantier + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..47f2832 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# HTT - HTML/Text Templating + +[![CI](https://github.com/jwdevantier/htt/actions/workflows/ci.yml/badge.svg)](https://github.com/jwdevantier/htt/actions/workflows/ci.yml) + +HTT is a templating language built around Lua that excels at generating code and configuration files. It combines the readability of traditional templating with the full power of Lua. + +## Key Features + +- **Component-Based**: Build complex templates by composing smaller, reusable components +- **Full Lua Integration**: Use Lua for logic, data transformation, and external module integration +- **Smart Indentation**: Maintains correct indentation across component nesting levels +- **Efficient**: Templates are compiled to Lua modules and loaded lazily + +## Documentation + +Visit [https://jwdevantier.github.io/htt/](https://jwdevantier.github.io/htt/) for: +- Getting started guide +- Syntax reference +- Examples and best practices +- Detailed explanations of components, modules, and debugging diff --git a/api/api_desc.lua b/api/api_desc.lua index ce9456b..0818d32 100644 --- a/api/api_desc.lua +++ b/api/api_desc.lua @@ -165,7 +165,7 @@ local tcp_stream = { content = { { name = "recv", - type = "function", + type = "method", params = { { name = "buf", type = t_tcp_buffer, summary = "Buffer of data to send" }, { name = "n", type = "integer", summary = "if provided, read exactly `n` bytes" }, @@ -180,7 +180,7 @@ local tcp_stream = { }, { name = "recv_at_least", - type = "function", + type = "method", params = { { name = "buf", type = t_tcp_buffer, summary = "Buffer of data to send" }, { name = "n", type = "integer", summary = "read at least this many bytes" }, @@ -195,7 +195,7 @@ local tcp_stream = { }, { name = "send", - type = "function", + type = "method", params = { { name = "buffer", type = t_tcp_buffer, summary = "Buffer" }, { name = "n", type = "integer?", summary = "if set, send the first n bytes, not entire buffer" }, @@ -216,7 +216,7 @@ local tcp_buffer = { content = { { name = "remaining", - type = "function", + type = "method", params = { }, desc = { @@ -230,7 +230,7 @@ local tcp_buffer = { }, { name = "size", - type = "function", + type = "method", params = { }, desc = { @@ -244,7 +244,7 @@ local tcp_buffer = { }, { name = "set_size", - type = "function", + type = "method", params = { { name = "N", type = "integer", "Desired size of the buffer, in bytes" } }, @@ -259,7 +259,7 @@ local tcp_buffer = { }, { name = "seek", - type = "function", + type = "method", params = { { name = "N", type = "integer", summary = "The byte offset to seek to" } }, @@ -271,7 +271,7 @@ local tcp_buffer = { }, { name = "tell", - type = "function", + type = "method", params = { }, desc = { @@ -283,7 +283,7 @@ local tcp_buffer = { }, { name = "write_string", - type = "function", + type = "method", params = { { name = "str", type = "string", summary = "the string to write" } }, @@ -297,7 +297,7 @@ local tcp_buffer = { }, { name = "write_bool", - type = "function", + type = "method", params = { { name = "val", type = "boolean", summary = "the value to write" } }, @@ -311,7 +311,7 @@ local tcp_buffer = { }, { name = "read_string", - type = "function", + type = "method", params = { { name = "len", type = "integer", summary = "length, in bytes, of string" } }, @@ -326,7 +326,7 @@ local tcp_buffer = { }, { name = "read_bool", - type = "function", + type = "method", params = { }, desc = { @@ -354,7 +354,7 @@ for _, signedness in ipairs({ "i", "u" }) do end table.insert(tcp_buffer.content, { name = string.format("write_%s%s%s", signedness, bits, order), - type = "function", + type = "method", params = { { name = "val", type = "integer", summary = "value to write" }, }, @@ -386,7 +386,7 @@ for _, signedness in ipairs({ "i", "u" }) do end table.insert(tcp_buffer.content, { name = string.format("read_%s%s%s", signedness, bits, order), - type = "function", + type = "method", params = { }, desc = { @@ -690,7 +690,7 @@ m_fs.content = { content = { { name = "path", - type = "function", + type = "method", params = { }, desc = { @@ -702,7 +702,7 @@ m_fs.content = { }, { name = "make_path", - type = "function", + type = "method", params = { { name = "subpath", type = "path", summary = "path of dirs, relative to dir, to create" }, }, @@ -718,7 +718,7 @@ m_fs.content = { }, { name = "open_dir", - type = "function", + type = "method", params = { { name = "subpath", type = "string", summary = "path relative to dir of directory to open" }, }, @@ -732,7 +732,7 @@ m_fs.content = { }, { name = "parent", - type = "function", + type = "method", params = { }, desc = { @@ -745,7 +745,7 @@ m_fs.content = { }, { name = "list", - type = "function", + type = "method", params = {}, desc = { "Return iterator to loop over all items in directory.", @@ -757,7 +757,7 @@ m_fs.content = { }, { name = "walk", - type = "function", + type = "method", params = {}, desc = { "Return iterator for recursively iterating through all contents nested under dir.", @@ -769,7 +769,7 @@ m_fs.content = { }, { name = "remove", - type = "function", + type = "method", params = { { name = "subpath", type = "string?", summary = "(optional) a path relative to this directory. Otherwise this directory." } }, @@ -785,7 +785,7 @@ m_fs.content = { }, { name = "exists", - type = "function", + type = "method", params = { { name = "subpath", type = "string?", summary = "(optional) either check subpath relative to directory, or directory itself" }, }, @@ -799,7 +799,7 @@ m_fs.content = { }, { name = "touch", - type = "function", + type = "method", params = { { name = "subpath", type = "string", summary = "path of file to create, relative to directory" }, }, @@ -1102,10 +1102,31 @@ local m_json = { } } +local htt_component = { + name = "Component", + type = "type", + content = { + { + name = "is", + type = "function", + params = { + { name = "value", type = "any", summary = "some value to check" }, + }, + desc = { + "Check whether value is a HTT template component" + }, + returns = { + { type = "boolean", summary = "true if a ComponentInstance, false otherwise" }, + } + }, + }, +} + htt.name = "htt" htt.summary = "HTT utility API" htt.class = "htt" htt.content = { + htt_component, m_str, m_env, m_tcp, diff --git a/api/typestub.htt b/api/typestub.htt index 88cef60..874e032 100644 --- a/api/typestub.htt +++ b/api/typestub.htt @@ -1,6 +1,6 @@ % -- see typestub.htt.lua for helper functions -% @component Description +% % if type(ctx.val) == "string" then ---{{ctx.val}} % elseif type(ctx.val) == "table" then @@ -8,26 +8,26 @@ ---{{line}} % end % end -% @end +% -% @component Alias +% {{@ M.Description {val = ctx.alias.desc or ctx.alias.summary} }} ---@alias {{ctx.alias.name}} {{ctx.alias.def}} -% @end +% -% @component FnArgs +% % if #ctx > 0 then {{ctx[1].name}} % for i = 2, #ctx do ~>, {{ctx[i].name}} % end % end -% @end +% -% @component Function +% {{@ M.Description {val = ctx.fn.desc or ctx.fn.summary} }} % for _, param in ipairs(ctx.fn.params) do ---@param {{param.name}} {{param.type}} {{param.summary}} @@ -40,10 +40,10 @@ % end % end function {{htt.str.join('.', ctx.parent)}}{{ctx.sep or '.'}}{{ctx.fn.name}}({{@ M.FnArgs ctx.fn.params }}) end -% @end +% -% @component Type +% % local parent = concat(ctx.parent, ctx.type.name) ---@class {{htt.str.join('.', ctx.parent)}}.{{ctx.type.name}} % for _, field in ipairs(ctx.type.fields or {}) do @@ -56,26 +56,29 @@ function {{htt.str.join('.', ctx.parent)}}{{ctx.sep or '.'}}{{ctx.fn.name}}({{@ {{htt.str.join('.', ctx.parent)}}.{{ctx.type.name}} = {} % end % for _, item in ipairs(ctx.type.content or {}) do -% if item.type == "function" then +% if item.type == "method" then {{@ M.Function {fn = item, parent = parent, sep = ":"} }} +% elseif item.type == "function" then + +{{@ M.Function {fn = item, parent = parent, sep = "."} }} % else % error(string.format("expects .content elems of type to be 'function', got '%s'", item.type)) % end -- if % end -- for -% @end +% -% @component Constant +% % if ctx.val.desc then {{@ Description {val = ctx.val.desc} }} % end ---@type {{ctx.val.luatype}} {{htt.str.join('.', ctx.parent)}}.{{ctx.val.name}} = nil -% @end +% -% @component ModuleContent +% % for _, item in ipairs(ctx.mod.content) do % if item.type == "module" then @@ -95,10 +98,10 @@ function {{htt.str.join('.', ctx.parent)}}{{ctx.sep or '.'}}{{ctx.fn.name}}({{@ {{@ M.Constant {val = item, parent = ctx.parent} }} % end -- if % end -- for -% @end +% -% @component Module +% % local parent = concat(ctx.parent, ctx.mod.name) % local mod_name = pascalCase("htt", ctx.mod.name, "module") % if ctx.mod.desc then @@ -107,25 +110,34 @@ function {{htt.str.join('.', ctx.parent)}}{{ctx.sep or '.'}}{{ctx.fn.name}}({{@ ---@class {{mod_name}} {{htt.str.join('.', ctx.parent)}}.{{ctx.mod.name}} = {} {{@ ModuleContent { mod = ctx.mod, parent = parent } }} -% @end +% -% @component TopLevel +% ---@meta +---Render component to file at `fpath`. +---@param component function the component to render +---@param fpath string relative path to file +---@param ctx table? context (arguments) to pass to the component +function render(component, fpath, ctx) end + ---{{ctx.mod.summary}} ---@class {{ctx.mod.class}} % for _, item in ipairs(ctx.mod.content) do -% if item.type == "module" then + % if item.type == "module" then ---@field {{item.name}} {{pascalCase(ctx.mod.class, item.name, "module")}} {{item.summary}} -% end + % end % end {{ctx.mod.class}} = {} % for _, item in ipairs(ctx.mod.content) do -% if item.type == "module" then + % if item.type == "module" then {{@ Module { mod = item, parent = {"htt"} } }} + % elseif item.type == "type" then + +{{@ M.Type {type = item, parent = {"htt"}} }} + % end % end -% end -% @end +% diff --git a/docs/api_module.htt b/docs/api_module.htt index 010c4c8..c8fe7eb 100644 --- a/docs/api_module.htt +++ b/docs/api_module.htt @@ -1,6 +1,6 @@ -% @code +%%% local T = require "tags" -% @end +%%% % -- function % -- { @@ -49,7 +49,7 @@ local T = require "tags" % -- written as a NO-OP in case description is nil -% @component Description +% % if type(ctx.val) == "string" then

{{ctx.val}}

@@ -65,38 +65,38 @@ local T = require "tags" % end

% end -% @end +%
-% @component EntrySubHdr +%
{{ctx.text}}
-% @end +%
-% @component Tag +% {{ctx.text}} -% @end +% -% @component Alias +% {{@ Description {val = ctx.item.desc}}} {{@ EntrySubHdr {text="Definition"}}} {{ctx.item.def}} -% @end +% -% @component AliasPreview +% {{ctx.item.name}} -% @end +% -% @component ParamType +% {{ctx.param.type}} -% @end +% -% @component FunctionSignature +% % local param_renderer = ctx.param_renderer {{ctx.fn.name}}( % if #ctx.fn.params > 0 then @@ -114,10 +114,10 @@ local T = require "tags" % else ~>nil % end -% @end +% -% @component Function +% {{@ Description {val = ctx.item.desc}}} % if #ctx.item.params > 0 then {{@ EntrySubHdr {text="Parameters"}}} @@ -149,29 +149,29 @@ local T = require "tags" % else No return value % end -% @end +% -% @component FunctionPreview +% ~>{{@ FunctionSignature {lookup = ctx.lookup, fn = ctx.item, param_renderer = ParamType}}} -% @end +% -% @component Constant +% {{@ Description {val = ctx.item.desc}}} {{@ EntrySubHdr {text="Type"}}} {{ctx.item.luatype}} -% @end +% -% @component ConstantPreview +% {{ctx.item.name}} -% @end +% -% @component Type +% % if ctx.item.fields ~= nil and #ctx.item.fields > 0 then {{@ EntrySubHdr {text="Fields"}}} -% @end +% -% @component menu +% % for _, entry in pairs(model.site) do % if model.is_section(entry) then {{@ menuSection { section = entry, page = ctx.page } }} % elseif model.is_page(entry) then % local x = entry.refid == ctx.page.refid and " font-bold" or "" - % print("entry(" .. entry.refid .. "), ctx.page(".. ctx.page.refid .. ") ->" .. tostring(x))
  • {{ entry.title }}
  • % end -- if % end -- for -% @end +%
    -% @component base +% % local section = model.ref2section[ctx.page.refid] @@ -345,4 +343,4 @@ document.addEventListener('DOMContentLoaded', function() { -% @end +% diff --git a/docs/common.htt.lua b/docs/common.htt.lua index 01a06b3..afb6d2b 100644 --- a/docs/common.htt.lua +++ b/docs/common.htt.lua @@ -1,33 +1,28 @@ -function extract_component_source(file_path, component_name, include_directives) +function extract_component_source(file_path, component_name, include_tags) local file = io.open(file_path, "r") if not file then error("Could not open file: " .. file_path) end local lines = {} - local nested_level = 0 local capturing = false - local component_pattern = "^%%%s*@component%s+" .. component_name .. "$" + local component_start = "^%s*%%%s*%<" .. component_name .. "%>$" + local component_end = "^%s*%%%s*%$" for line in file:lines() do if capturing then - if nested_level > 0 or not line:match("^%%%s*@end") then + if line:match(component_end) then + if include_tags then + table.insert(lines, line) + end + capturing = false + break -- done capturing component + else table.insert(lines, line) - elseif include_directives and nested_level == 0 then - table.insert(lines, line) - break - elseif nested_level == 0 then - break - end - - if line:match("^%%%s*@%w+") and not line:match("^%%%s*@end") then - nested_level = nested_level + 1 - elseif line:match("^%%%s*@end") then - nested_level = nested_level - 1 end - elseif line:match(component_pattern) then + elseif line:match(component_start) then capturing = true - if include_directives then + if include_tags then table.insert(lines, line) end end @@ -41,3 +36,4 @@ function extract_component_source(file_path, component_name, include_directives) return table.concat(lines, "\n") end + diff --git a/docs/debug.htt b/docs/debug.htt index 2c87684..5bfc3de 100644 --- a/docs/debug.htt +++ b/docs/debug.htt @@ -1,17 +1,16 @@ -% @code +%%% local C = require "//common.htt" local T = require "tags" local e = require "elems" local code = T.code local xref = C.xref +%%% -% @end - -% @component httLibraryLoaderNote +% In this case, that is in the bundled library code ({{code "[[HTT Library]]"}}), line 432. In the copy of HTT used, this line marks the start of the loader function which HTT installs to intercept calls to {{code 'require "//<path/to/template>.htt'}}, which compile HTT templates into lua modules before loading them in as usual. -% @end +% -% @component httCompiledFileNote +%

    Every HTT template file is compiled, and the resulting Lua module is saved on disk. The result of compiling {{code "foo.htt"}} is saved as {{code "foo.out.lua"}} in the same directory. ~>

    @@ -19,10 +18,10 @@ Every HTT template file is compiled, and the resulting Lua module is saved on di

    The compiled output should make for easy reading, and sometimes a difficult to understand error becomes obvious when you read the relevant line in the compiled output. ~>

    -% @end +%
    -% @component main +%

    You will make mistakes. Here we will discuss the types of errors you will encounter and how to read the error output. ~>

    @@ -30,7 +29,7 @@ You will make mistakes. Here we will discuss the types of errors you will encoun {{T.h4 "Types of errors"}}

    The types of error you will encounter are: -% @code +%%% local issues = e.ul { e.li {"Failure to load the initial script"}, e.li {"Failue to compile some template file"}, @@ -39,7 +38,7 @@ local issues = e.ul { }}, e.li {"An error during evaluation of the Lua code"}, } -% @end +%%% {{@ C.list issues }} ~>

    @@ -93,7 +92,7 @@ The initial script is loaded in a special way. For all other modules, whether wr

    In this case, we wrote {{code "if x"}}, not {{code "if x then"}} as is required: ~>

    -{{@ C.compSrc {file = "//examples/debug/err-stx.htt", component = "one", include_directives = true} }} +{{@ C.compSrc {file = "//examples/debug/err-stx.htt", component = "one", include_tags = true} }}

    @@ -129,8 +128,8 @@ Template code can cause errors. If an error happens, you can determine where by In this example, we have two components, {{code "parent"}} and {{code "child"}}, both defined in {{code "//examples/debug/err-exec.htt"}}. The {{code "parent"}} calls {{code "child"}} which causes a runtime error. ~>

    -{{@ C.compSrc {file = "//examples/debug/err-exec.htt", component = "parent", include_directives = true} }} -{{@ C.compSrc {file = "//examples/debug/err-exec.htt", component = "child", include_directives = true} }} +{{@ C.compSrc {file = "//examples/debug/err-exec.htt", component = "parent", include_tags = true} }} +{{@ C.compSrc {file = "//examples/debug/err-exec.htt", component = "child", include_tags = true} }}

    Note in output that we see the error itself, and before that, we can see the call-stack for components, {{code "//examples/debug/err-exec.htt.parent"}} is the {{code "parent"}} component, and the stack shows how it calls {{code "//examples/debug/err-exec.htt.child"}} ({{code "child"}}). @@ -141,4 +140,4 @@ Note in output that we see the error itself, and before that, we can see the cal

    ~>

    -% @end +%
    diff --git a/docs/ex-go-ast.htt b/docs/ex-go-ast.htt index 2ee901a..17b55ef 100644 --- a/docs/ex-go-ast.htt +++ b/docs/ex-go-ast.htt @@ -1,19 +1,19 @@ -% @code +%%% local C = require "//common.htt" local T = require "tags" local code = T.code local ast1 = "//examples/ast/ast1.htt" -% @end +%%% -% @component aboutLuaTables +%

    The final indentation of any given line is the sum of: Lua tables are a hybrid data-structure which can work both as an list and a map. However, map entries are unordered, which isn't desirable when generating code. Hence I often define a (list-like) table of (associative) tables elements. ~>

    -% @end +%
    -% @component main +%

    This example implements some of the AST nodes from the book, {{@ C.url { ref = "https://interpreterbook.com/", label = "Writing an Interpreter in Go"}}}, specifically as implemented in {{@ C.url { ref = "https://github.com/kitasuke/monkey-go/blob/e1716fdf2e445456116fc844023a685521202f91/ast/ast.go", label = "this git repository"}}}. ~>

    @@ -29,7 +29,7 @@ Beyond this, nodes implement {{code "String"}} and {{code "TokenLiteral"}}, and We start by creating a template file, {{code "ast.htt"}} and defining the package, implementing the base types and so on. All of this is regular Go code: ~>

    -{{@ C.compSrc {file = ast1, component = "main", include_directives = true} }} +{{@ C.compSrc {file = ast1, component = "main", include_tags = true} }} Rendering this essentially produces the same output: {{@ C.eval {file = "//examples/ast/ast1.htt", component = "main"}}} @@ -61,12 +61,12 @@ The code in {{code "ast.htt.lua"}} is, as the name implies, regular Lua code. Th

    To use this data, let's define a component, {{code "node"}}, and just render out the name of the component for now: ~>

    -{{@ C.compSrc {file = "//examples/ast/ast2.htt", component = "node", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast2.htt", component = "node", include_tags = true} }} {{T.h3 "Defining how to render all nodes"}} To render all nodes, we define a component {{code "render_nodes"}}, which loops over each entry in the model (the {{code "nodes"}} function in {{code "ast.htt.lua"}}) and renders it: -{{@ C.compSrc {file = "//examples/ast/ast2.htt", component = "render_nodes", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast2.htt", component = "render_nodes", include_tags = true} }}

    You could add these lines directly to {{code "main"}}. I normally would. But this way I can show you changes to this component in isolation. @@ -80,7 +80,7 @@ Finally, we call {{code "render_nodes"}} from our {{code "main"}} component. Sin Our main component, thus becomes: ~>

    -{{@ C.compSrc {file = "//examples/ast/ast2.htt", component = "main", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast2.htt", component = "main", include_tags = true} }} {{T.h4 "Output"}} {{@ C.eval {file = "//examples/ast/ast2.htt", component = "main"}}} @@ -90,13 +90,13 @@ Success! We see the names of the nodes we defined in our model below the Go code {{T.h2 "Rendering the Node struct"}} Let's start by rendering the struct for east AST node: -{{@ C.compSrc {file = "//examples/ast/ast3.htt", component = "node", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast3.htt", component = "node", include_tags = true} }} The nodes now render as: {{@ C.eval {file = "//examples/ast/ast3.htt", component = "render_nodes_nospace"}}} Before moving on, let's add some whitespace between components. You can do this in multiple ways, but inserting an empty line ahead of rendering each component in loop body of {{code "render_nodes"}} works well: -{{@ C.compSrc {file = "//examples/ast/ast3.htt", component = "render_nodes", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast3.htt", component = "render_nodes", include_tags = true} }} Now we get: {{@ C.eval {file = "//examples/ast/ast3.htt", component = "render_nodes"}}} @@ -105,7 +105,7 @@ Now we get: For now, we only handle nodes whose {{code "String"}} and {{code "TokenLiteral"}} implementations both return the value of {{code ".Token.Literal"}}. Beyond that, we must implement either the empty function {{code "expressionNode()"}} or {{code "statementNode()"}}, depending on the type of node, as identified by the {{code "is_expr"}} field for the node in our model: -{{@ C.compSrc {file = "//examples/ast/ast4.htt", component = "node", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast4.htt", component = "node", include_tags = true} }} Now, the code for the nodes themselves becomes: {{@ C.eval {file = "//examples/ast/ast4.htt", component = "render_nodes"}}} @@ -129,7 +129,7 @@ To support this, we need a way to reference the components to use for implementi All templates can use the variable {{code "M"}} to refer to their own modules. So we can just send this along to the {{code "nodes()"}} function which returns the model: ~>

    -{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "render_nodes", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "render_nodes", include_tags = true} }}

    @@ -148,7 +148,7 @@ With this change, we update the model, expanding the number of nodes we implemen The key change in the template is how we implement {{code "node"}} Notice now that we check if {{code "ctx.string_fn"}} is defined, and if so, renders that component: ~>

    -{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "node", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "node", include_tags = true} }}

    This demonstrates how HTT components can be passed as arguments to other components which can render them. Also notice that since all compontents take exactly one table argument, we can pass all arguments along by passing {{code "ctx"}}. @@ -164,11 +164,11 @@ The final change to the {{code "ast.htt"}} template file is implementing the com There is nothing to these components aside from noting that since we passed the entire {{code "ctx"}} from the {{code "node"}} component along to these, we still have access to attributes like {{code "short"}} from the model describing the node. ~>

    -{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "return_statement_string", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "return_statement_string", include_tags = true} }} -{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "expr_statement_string", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "expr_statement_string", include_tags = true} }} -{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "prefix_expr_string", include_directives = true} }} +{{@ C.compSrc {file = "//examples/ast/ast5.htt", component = "prefix_expr_string", include_tags = true} }} @@ -190,4 +190,4 @@ To achieve this, we would:
  • in {{code "ast_model.lua"}}, change all references to the model {{code "m.component"}} to {{code "tpl.component"}}.
  • ~>

    -% @end +%
    diff --git a/docs/examples/ast/ast1.htt b/docs/examples/ast/ast1.htt index ff118cb..fe86cc1 100644 --- a/docs/examples/ast/ast1.htt +++ b/docs/examples/ast/ast1.htt @@ -1,5 +1,5 @@ -% @component main +%
    package ast; import ( @@ -23,4 +23,4 @@ type Expression interface { Node expressionNode() } -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/docs/examples/ast/ast2.htt b/docs/examples/ast/ast2.htt index 1ec9f01..8d5c58b 100644 --- a/docs/examples/ast/ast2.htt +++ b/docs/examples/ast/ast2.htt @@ -1,15 +1,15 @@ -% @component node +% {{ctx.name}} -% @end +% -% @component render_nodes +% % for _, elem in ipairs(nodes()) do {{@ node elem }} % end -% @end +% -% @component main +%
    package ast; import ( @@ -35,4 +35,4 @@ type Expression interface { } {{@ render_nodes {} }} -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/docs/examples/ast/ast3.htt b/docs/examples/ast/ast3.htt index b39f8b6..28dfaae 100644 --- a/docs/examples/ast/ast3.htt +++ b/docs/examples/ast/ast3.htt @@ -1,28 +1,28 @@ -% @component node +% type {{ctx.name}} struct { Token token.Token % for _, field in ipairs(ctx.fields) do {{field.name}} {{field.type}} % end } -% @end +% % -- first take -% @component render_nodes_nospace +% % for _, elem in ipairs(nodes()) do {{@ node elem }} % end -% @end +% -% @component render_nodes +% % for _, elem in ipairs(nodes()) do {{@ node elem }} % end -% @end +% -% @component main +%
    package ast; import ( @@ -48,4 +48,4 @@ type Expression interface { } {{@ render_nodes {} }} -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/docs/examples/ast/ast4.htt b/docs/examples/ast/ast4.htt index d2023ac..d0860e8 100644 --- a/docs/examples/ast/ast4.htt +++ b/docs/examples/ast/ast4.htt @@ -1,9 +1,9 @@ -% @component base_literal +% return {{ctx.short}}.Token.Literal -% @end +% -% @component node +% type {{ctx.name}} struct { Token token.Token % for _, field in ipairs(ctx.fields) do @@ -24,16 +24,16 @@ func ({{ctx.short}} *{{ctx.name}}) TokenLiteral() string { func ({{ctx.short}} *{{ctx.name}}) String() string { return {{ctx.short}}.Token.Literal } -% @end +% -% @component render_nodes +% % for _, elem in ipairs(nodes()) do {{@ node elem }} % end -% @end +% -% @component main +%
    package ast; import ( @@ -59,4 +59,4 @@ type Expression interface { } {{@ render_nodes {} }} -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/docs/examples/ast/ast5.htt b/docs/examples/ast/ast5.htt index 61d5e8d..bb18910 100644 --- a/docs/examples/ast/ast5.htt +++ b/docs/examples/ast/ast5.htt @@ -1,4 +1,4 @@ -% @component node +% type {{ctx.name}} struct { Token token.Token % for _, field in ipairs(ctx.fields) do @@ -23,9 +23,9 @@ func ({{ctx.short}} *{{ctx.name}}) String() string { return {{ctx.short}}.Token.Literal % end } -% @end +% -% @component return_statement_string +% var out bytes.Buffer out.WriteString(rs.Token.Literal + " ") @@ -36,16 +36,16 @@ if rs.ReturnValue != nil { out.WriteString(";") return out.String() -% @end +% -% @component expr_statement_string +% if {{ctx.short}}.Expression != nil { return {{ctx.short}}.Expression.String() } return "" -% @end +% -% @component prefix_expr_string +% var out bytes.Buffer out.WriteString("(") @@ -54,16 +54,16 @@ out.WriteString({{ctx.short}}.Right.String()) out.WriteString(")") return out.String() -% @end +% -% @component render_nodes +% % for _, elem in ipairs(nodes(M)) do {{@ node elem }} % end -% @end +% -% @component main +%
    package ast; import ( @@ -89,4 +89,4 @@ type Expression interface { } {{@ render_nodes {} }} -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/docs/examples/debug/err-compile.htt b/docs/examples/debug/err-compile.htt index 682d63e..abae7a6 100644 --- a/docs/examples/debug/err-compile.htt +++ b/docs/examples/debug/err-compile.htt @@ -1,6 +1,6 @@ This line causes a compile failure -% @component one +% % local x = false % if x then % end -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/docs/examples/debug/err-exec.htt b/docs/examples/debug/err-exec.htt index 4f0fb52..f5994c0 100644 --- a/docs/examples/debug/err-exec.htt +++ b/docs/examples/debug/err-exec.htt @@ -1,10 +1,10 @@ -% @component child +% % error("I am raising an error") -% @end +% -% @component parent +% % local x = false % if x then % end Call child {{@ child {} }} -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/docs/examples/debug/err-stx.htt b/docs/examples/debug/err-stx.htt index c4bd0cd..03da967 100644 --- a/docs/examples/debug/err-stx.htt +++ b/docs/examples/debug/err-stx.htt @@ -1,5 +1,5 @@ -% @component one +% % local x = false % if x % end -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/docs/examples/quick-start/components.htt b/docs/examples/quick-start/components.htt index 4dbee4a..460cc09 100644 --- a/docs/examples/quick-start/components.htt +++ b/docs/examples/quick-start/components.htt @@ -1,68 +1,68 @@ -% @component helloWorld +% Hello, John! -% @end +% -% @component loop3 +% % for i = 1, 3 do hello! % end -% @end +% -% @component ifRender +% % if 1 == 2 then Math is broken! % else Phew! Math works % end -% @end +% -% @component varExample +% % local name = "Peter" Hello, {{name}}! 2 + 2 is {{2 + 2}} -% @end +% -% @component child +% Sincerely hello - Child -% @end +% -% @component parent +% let's call the child: {{@ child {} }} -% @end +% -% @component middleChild +% --middle child {{@ child {} }} -% @end +% -% @component callWithIndentation +% {{@ child {} }} {{@ middleChild {} }} {{@ child {} }} -% @end +% -% @component helloName +% Hello, {{ctx.name or "John Doe"}}! -% @end +% -% @component callHelloNameNoArg +% {{@ helloName {} }} -% @end +% -% @component callHelloNamePeter +% {{@ helloName {name = "Peter"} }} -% @end +% -% @component lineCont +% hello ~>world! hello ~> world... -% @end +% -% @component lineContArr +% % local sep = ctx.separator or "," [ % for i, v in ipairs(ctx.lst) do @@ -70,8 +70,40 @@ hello ~> "{{v}}"{{not is_last and sep or ""}} % end ~> ] -% @end +% -% @component callLineContArr +% {{@ lineContArr {lst = {"one", "two", "three"} } }} -% @end +% + +% +%%% +local names = { + "john", + "jane" +} +%%% +Hello {{names[1]}}, {{names[2]}} +% + + +% -- nested components example +% + + + + + {{ctx.title}} + + + {{@ ctx.body {} }} + + +% + +% +% +

    Something I like to do

    +%
    +{{@ page {title = "my hobbies", body = content}}} +%
    diff --git a/docs/examples/syntax/def-component.txt b/docs/examples/syntax/def-component.txt index c51451f..514a32c 100644 --- a/docs/examples/syntax/def-component.txt +++ b/docs/examples/syntax/def-component.txt @@ -1,6 +1,6 @@ -% @component component_name +% ... -% @end +% component_name = [a-zA-Z_][a-zA-Z0-9_]* diff --git a/docs/examples/syntax/examples.htt b/docs/examples/syntax/examples.htt index f2fd35e..d71bb0d 100644 --- a/docs/examples/syntax/examples.htt +++ b/docs/examples/syntax/examples.htt @@ -1,18 +1,22 @@ -% @component luaLine +% % --[[ lua code here ]]-- -% @end +% -% @component myComponent +% ... content here :) -% @end +% -% @component luaCodeBlock -% @code +% +%%% -- lua code here -% @end -% @end +%%% +% -% @component luaExpr -{{hello}} -% @end \ No newline at end of file +% +Hello, {{ctx.name}} +% + +% +{{@ greeting {name = "John"}}} +% diff --git a/docs/examples/syntax/grammar.txt b/docs/examples/syntax/grammar.txt index 5b26324..11f331a 100644 --- a/docs/examples/syntax/grammar.txt +++ b/docs/examples/syntax/grammar.txt @@ -7,15 +7,12 @@ top = | ws* '%' lua nl code = - | code-open nl + | code-delim nl . lua nl - . code-close nl + . code-delim nl - code-open = - | ws* '%' ws+ '@code' - - code-close = - | ws* '%' ws+ '@end' + code-delim = + | ws* '%%%' ws* component = | component-open nl @@ -23,7 +20,7 @@ top = . component-close nl component-open = - | ws* '%' ws+ '@component' component_name + | ws* '%' ws+ '<' component_name '>' component_name = | [a-zA-Z_][a-zA-Z0-9_]* @@ -34,7 +31,7 @@ top = | code component-close = - | ws* '%' ws+ '@end' + | ws* '%' ws+ '</' component_name '>' text-line = | ws* line-continuation? element* nl diff --git a/docs/htt-intro.htt b/docs/htt-intro.htt index 8ea4006..8b01c4d 100644 --- a/docs/htt-intro.htt +++ b/docs/htt-intro.htt @@ -1,11 +1,11 @@ -% @code +%%% local C = require "//common.htt" local T = require "//tags" local code = T.code local i = T.i -% @end +%%% -% @component main +%
    % -- title is "What is HTT?"

    HTT is a code-generator. It is built to help you generate code or configuration files from some high-level description which you either write yourself or get from elsewhere. @@ -30,4 +30,4 @@ Finally, code generation is also an excellent way to address shortcomings of con If your DSL files become very long, verbose or difficult to read, perhaps it is time to do some pre-processing in a more capable language, like Lua, and generate simpler DSL files instead. ~>

    -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/docs/htt.tmLanguage.json b/docs/htt.tmLanguage.json index a926814..111dba9 100644 --- a/docs/htt.tmLanguage.json +++ b/docs/htt.tmLanguage.json @@ -6,7 +6,10 @@ "include": "#lua-code-block" }, { - "include": "#directive-line" + "include": "#component-begin" + }, + { + "include": "#component-end" }, { "include": "#lua-line" @@ -23,22 +26,16 @@ ], "repository": { "lua-code-block": { - "begin": "^(\\s*%\\s*)(@code)\\s*$", + "begin": "^(\\s*%%%)\\s*$", "beginCaptures": { "1": { - "name": "punctuation.definition.directive.htt" - }, - "2": { - "name": "keyword.control.directive.htt" + "name": "punctuation.section.embedded.begin.htt" } }, - "end": "^(\\s*%\\s*)(@end)\\s*$", + "end": "^(\\s*%%%)\\s*$", "endCaptures": { "1": { - "name": "punctuation.definition.directive.htt" - }, - "2": { - "name": "keyword.control.directive.htt" + "name": "punctuation.section.embedded.begin.htt" } }, "name": "meta.embedded.block.lua", @@ -48,21 +45,37 @@ } ] }, - "directive-line": { - "match": "^(\\s*%\\s*)(@\\w+)\\s*(.*)", + "component-begin": { + "match": "^(\\s*%\\s*)(<)(\\w+)(>)", + "captures": { + "1": { + "name": "punctuation.definition.directive.htt" + }, + "2": { + "name": "punctuation.definition.tag.begin.htt" + }, + "3": { + "name": "entity.name.class.htt" + }, + "4": { + "name": "punctuation.definition.tag.end.htt" + } + } + }, + "component-end": { + "match": "^(\\s*%\\s*)(<\\/)(\\w+)(>)", "captures": { "1": { "name": "punctuation.definition.directive.htt" }, "2": { - "name": "keyword.control.directive.htt" + "name": "punctuation.definition.tag.begin.htt" }, "3": { - "patterns": [ - { - "include": "source.lua" - } - ] + "name": "entity.name.class.htt" + }, + "4": { + "name": "punctuation.definition.tag.end.htt" } } }, diff --git a/docs/model.lua b/docs/model.lua index b3a65ae..b6b2010 100644 --- a/docs/model.lua +++ b/docs/model.lua @@ -160,7 +160,7 @@ M.site_post_process = function(site_prefix) if M.is_section(entry) then local section = entry for _, page in ipairs(section.pages) do - M.ref2page[page.refid] = entry + M.ref2page[page.refid] = page M.ref2section[page.refid] = section compute_slug(site_prefix, page, section) end diff --git a/docs/modules-and-files.htt b/docs/modules-and-files.htt index 8780d45..29363eb 100644 --- a/docs/modules-and-files.htt +++ b/docs/modules-and-files.htt @@ -1,36 +1,36 @@ -% @code +%%% local C = require "//common.htt" local T = require "//tags" local code = T.code local i = T.i local xref = C.xref -% @end +%%% -% @component ExLuaPackagePath -% @code +% +%%% content = [[$ cat /tmp/test.lua print(package.path) $ htt /tmp/test.lua /tmp/?.lua;/tmp/?/init.lua; ]] -% @end +%%% {{@ C.codebox {c = C.text, c_args = {text = content} } }} -% @end +% -% @component ExRequireLuaModule -% @code +% +%%% local content = [[-- look for "foo.lua" or "foo/init.lua" local mod = require("foo") -- look for "bar/foo.lua" or "bar/foo/init.lua" local mod2 = require("bar.foo") ]] -% @end +%%% {{@ C.codebox {c = C.text, c_args = {text = content} } }} -% @end +% -% @component ExRequireTemplate -% @code +% +%%% local content = [[ -- look for "foo.htt", relative to HTT root local mod = require("//foo.htt") @@ -38,11 +38,11 @@ local mod = require("//foo.htt") -- look for "bar/foo.htt", relative to HTT root local mod2 = require("//bar/foo.htt") ]] -% @end +%%% {{@ C.codebox {c = C.text, c_args = {text = content} } }} -% @end +% -% @component main +%

    This covers how HTT and indeed Lua resolves and imports code and how this relates to module names. This is important both when debugging errors and laying out your projects. ~>

    @@ -124,4 +124,4 @@ Basically, we write what looks like a relative path, using the Unix path-separat {{@ExRequireTemplate {} }} -% @end +%
    diff --git a/docs/quick-start.htt b/docs/quick-start.htt index 17d162b..a8ad58a 100644 --- a/docs/quick-start.htt +++ b/docs/quick-start.htt @@ -4,39 +4,20 @@ % local qex = "//examples/quick-start/components.htt" -% @component importNoteContent +%

    Notice the call to {{code "require"}}. HTT templates are transparently compiled to Lua as they are first imported. Read more about importing HTT templates {{@ C.xref {ref = "modules-and-files", bookmark = "import-htt", txt = "here"}}} ~>

    -% @end +%
    -% @component directiveNote -

    -HTT interprets lines where text after {{code "%"}} starts with {{code "@"}} differently. These lines are taken to be directives, of which there are currently 2: {{code "@component"}} and {{code "@code"}}. We will cover this more below. -~>

    -% @end - -% @component variablesFromCtxNote +%

    See the the "Context" section under "Components" for information on how to pass arguments to a component and how to access them. ~>

    -% @end +%
    -% @component indentationNote -

    The final indentation of any given line is the sum of: -~>

    - - -

    This sounds complex, but feels natural. Play around and you will see how indentation generally behaves as you would expect and want. -~>

    -% @end -% @component main +%
    {{T.h2 "Hello World"}}

    @@ -45,14 +26,14 @@ HTT interprets lines where text after {{code "%"}} starts with {{code "@"}} diff

    Let's create a script, {{code "test-htt.lua"}}: ~>

    -{{@ C.include {file = "examples/quick-start/test-htt.lua"} }} +{{@ C.include {file = "examples/quick-start/test-htt.lua", lang = "lua"} }} {{@ C.note {c = importNoteContent} }} Let's then fill out the template file, {{code "helloworld.htt"}}: -{{@ C.compSrc {file = qex, component = "helloWorld", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "helloWorld", include_tags = true} }} If you now run the command {{code "htt test-htt.lua"}}, then the {{code "result.txt"}} file will look like this: @@ -68,7 +49,16 @@ If you now run the command {{code "htt test-htt.lua"}}, then the {{code "result.

    HTT uses plain Lua for all logic inside of templates. Any line which starts with {{code "%"}} is taken to be a line of Lua code injected verbatim into the compiled template. ~>

    -{{@ C.note {title = "What about '% @component'?", c = directiveNote} }} +% +

    There are 2 exceptions:

    + +
      +
    1. {{code "%%%"}}-lines mark the start- and end of a block of Lua code
    2. +
    3. {{code "% <foo>"}} and {{code "% </foo>"}} mark the start- and end of a component, named {{code "foo"}} in this example. See more information below.
    4. +
    +

    Other than that, any line starting with {{code "%"}} really is just a line of plain Lua code!

    +% +{{@ C.note {c = note_component_lines, title = "Are all lines starting with '%' really a Lua line?"} }} {{T.h3 "Looping with Lua"}}

    We can use Lua for loops to repeat a block of output like so: @@ -94,9 +84,16 @@ If we render this component, we get: {{@ C.note {title = "Arguments & components", c = variablesFromCtxNote} }} +{{T.h3 "In-lining blocks of Lua code"}} +

    Generally, you should move larger pieces of logic (Lua code) out of your templates. However, it is possible to define blocks of code directly inside a component: +~>

    + +{{@ C.compSrc {file = qex, component = "ex_lua_block"} }} +{{@ C.eval {file = qex, component = "ex_lua_block"}}} + {{T.bookmark "components"}} {{T.h2 "Components"}} -

    Components are the unit of abstraction. Complex outputs should be built by composing smaller components into larger ones. +

    Template files can contain many components. A component is a small, reusable "mini-template", which can be called from other components and which, when called, can receive arguments, including other components. ~>

    {{T.h3 "Calling components from within components"}} @@ -106,12 +103,12 @@ If we render this component, we get:

    Given this component: ~>

    -{{@ C.compSrc {file = qex, component = "child", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "child", include_tags = true} }}

    Let's call it from another component. ~>

    -{{@ C.compSrc {file = qex, component = "parent", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "parent", include_tags = true} }}

    The output becomes: ~>

    @@ -123,7 +120,7 @@ If we render this component, we get:

    We first define a component which uses the argument {{code "name"}}, if provided. Note that all arguments passed to a component are exposed via the {{code "ctx"}} variable. {{code "ctx.name"}} is simply the Lua way of accessing the attribute {{code "name"}} on the {{code "ctx"}} table. ~>

    -{{@ C.compSrc {file = qex, component = "helloName", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "helloName", include_tags = true} }} {{ T.h4 "Calling component without name argument" }}

    Calling the component without no name:

    @@ -142,11 +139,11 @@ If we render this component, we get: {{T.h3 "Indentation and Components"}} A focus of HTT has been to get indentation "right". Before we summarize how it works, here's an expanded example: -{{@ C.compSrc {file = qex, component = "child", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "child", include_tags = true} }} -{{@ C.compSrc {file = qex, component = "middleChild", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "middleChild", include_tags = true} }} -{{@ C.compSrc {file = qex, component = "callWithIndentation", include_directives = true} }} +{{@ C.compSrc {file = qex, component = "callWithIndentation", include_tags = true} }}

    Rendering this out gives this output: ~>

    @@ -154,8 +151,31 @@ A focus of HTT has been to get indentation "right". Before we summarize how it w {{@ C.eval {file = qex, component = "callWithIndentation"}}} -{{@ C.note {title = "How indentation works", c = indentationNote} }} +% +

    The final indentation of any given line is the sum of: +~>

    +
      +
    • The indentation of the line itself within the component
    • +
    • The indentation of the line that called the component
    • +
    • The indentation of the line that called the parent component
    • +
    • ... and so on, all the way up the chain of component calls
    • +
    + +

    This sounds complex, but feels natural. Play around and you will see how indentation generally behaves as you would expect and want. +~>

    +%
    +{{@ C.note {title = "How indentation works", c = note_indentation} }} + +{{T.h3 "Nested Components"}} +

    Components can be defined inside another component. This is useful whenever you write components which should not be used elsewhere. One example would be a site-generator, where we want to inject the page content into a full HTML5 page: +~>

    + +{{@ C.compSrc {file = qex, component = "page", include_tags = true} }} +{{@ C.compSrc {file = qex, component = "page_hobbies", include_tags = true} }} +

    +Notice how in {{code "page_hobbies"}}, we define a nested component, {{code "content"}}, which we pass along to the {{code "page"}} component. +~>

    {{T.h2 "Line Continuations"}}

    There will be times, usually when mixing output and Lua lines, where you want to build up a single line of output across multiple lines in the template. @@ -182,4 +202,4 @@ A focus of HTT has been to get indentation "right". Before we summarize how it w

    The line printing the separator uses a trick in Lua to simulate a ternary-if:

    {{code "$test and $if-truthy or $otherwise"}}.
    Alternatively, we could have used an if-block instead. ~>

    -% @end +%
    diff --git a/docs/setup.htt b/docs/setup.htt index c57774b..d437a8c 100644 --- a/docs/setup.htt +++ b/docs/setup.htt @@ -7,17 +7,17 @@ % local ul = e.ul -% @component main +%
    {{T.h2 "Installing HTT"}}

    HTT is available for all the major platforms: -% @code +%%% local platform_list = ul { li {"Linux (x86-64, Arm64)"}, li {"macOS (Apple Silicon)"}, li {"Windows (x86-64, Arm64)"}, } -% @end +%%% {{@ C.list platform_list }} ~>

    @@ -28,18 +28,18 @@ Head to the {{@url { ref = "https://github.com/jwdevantier/htt/releases", label {{T.h2 "Syntax Highlighting"}}

    HTT uses a custom DSL (domain-specific language) for writing templates. Currently, there is syntax highlighting support for the following editors: -% @code +%%% local editors = ul { li {url, {ref = "https://github.com/jwdevantier/htt-nvim", label = "Neovim"}}, li {url, {ref = "https://github.com/jwdevantier/htt-vscode", label = "Visual Studio Code"}} } -% @end +%%% {{@ C.list editors }} ~>

    {{T.h3 "Visual Studio Code"}}

    -% @code +%%% local install_steps_ = ol { li {"Open Visual Studio Code"}, li {"Open Extensions View:", ul { @@ -49,7 +49,7 @@ local install_steps_ = ol { li {[[Search for "HTT Templating Language"]]}, li {"Install"}, } -% @end +%%% {{@ C.list install_steps_ }} ~>

    @@ -66,4 +66,4 @@ HTT can configure {{@ url {ref = "https://github.com/LuaLS/lua-language-server",

    Run {{T.code "htt --init-lsp-conf"}} in the root directory of your project to generate the necessary (type-stub) files and configure the LSP. If you wish to write the files elsewhere, the {{T.code "-o"}}/{{T.code "--out-dir"}} flag can be used.

    -% @end +%
    diff --git a/docs/syntax-recap.htt b/docs/syntax-recap.htt index bbb473c..eac39fa 100644 --- a/docs/syntax-recap.htt +++ b/docs/syntax-recap.htt @@ -1,4 +1,4 @@ -% @code +%%% local C = require "//common.htt" local T = require "//tags" local e = require "elems" @@ -8,24 +8,14 @@ local xref = C.xref local raw = T.raw local stx = "//examples/syntax/examples.htt" -% @end +%%% -% @component grammarItem +% {{@ C.code {text = ctx.code}}} - {{ ctx.text }} -% @end +% -% @component stxComponent -{{ raw "% @end"}} -% @end - -% @component noteComponent -

    -Note how for components, the opening tag of the directive take the name of the component as an additional argument. -~>

    -% @end - -% @component main +%

    This section is just a minimal recap of the HTT templating syntax so you have the proper vocabulary in place and can visually recognize the various parts of the syntax. ~>

    @@ -37,37 +27,25 @@ To learn how to {{i "apply"}} this syntax and write actual templates, see the {{ Finally, you may actually want to see a condensed grammar definition. If so, click {{@ xref {ref="syntax-recap", bookmark="grammar", txt="here"} }} ~>

    -{{T.h3 "Literal Text"}} +{{T.h2 "Literal Text"}}

    The majority of the template will probably be literal text. Any text which is not triggering any of the syntax rules below is rendered exactly as you typed it in. This is what permits most templates to still be recognizable to people understanding the eventual output. ~>

    +{{T.h2 "Lua Code"}} {{T.h3 "Lua Line"}} -{{@ C.compSrc {file = stx, component = "luaLine"} }} +{{@ C.compSrc {file = stx, component = "lua_line"} }}

    -Lua lines begin with are lines whose first non-whitespace character is {{code "%"}}. The remainder of the line is handled as Lua code and inserted verbatim in the compiled Template's output. +Lua lines begin with the {{code "%"}} character. The remainder of the line is handled as Lua code and inserted verbatim in the compiled template. ~>

    -{{T.h3 "Directives"}} +{{T.h3 "Code Block"}}

    -Directives are blocks with a defined start- ({{code "% @<directive type>"}}) and end ({{code "% @end"}}). -Directives cannot partially overlap, but {{code "code"}}-directives can be nested inside {{code "component"}}-directives. +You can write blocks of verbatim Lua code by wrapping it in a {{code "%%%"}}: ~>

    - -{{T.h4 "Component"}} -{{@ C.compSrc {file = stx, component = "myComponent", include_directives = true} }} - -

    -References to {{code "ctx"}} within the component refers to the Lua table which holds all arguments passed to the component when called. -~>

    - -{{T.h4 "(Lua) Code"}} -

    -You can write blocks of verbatim Lua code by wrapping it in a {{code "@code"}} directive: -~>

    -{{@ C.compSrc {file = stx, component = "luaCodeBlock"} }} +{{@ C.compSrc {file = stx, component = "code_block"} }} {{T.h3 "(Lua) Expressions"}}

    @@ -77,16 +55,28 @@ Whenever you see {{code "{{ ... }}"}}, it is a Lua expression. Expressions are evaluated and {{code "tostring()"}} is called on their value and it is this value which is embedded in the output. ~>

    -{{T.h3 "Component Render Call"}} + +{{T.h2 "Components"}} +{{T.h3 "Component Blocks"}} +Components are defined using tags ({{code "<foo>"}}) +{{@ C.compSrc {file = stx, component = "my_component", include_tags = true} }} + +

    +Inside a component, {{code "ctx"}} refers to a lua table holding all arguments passed to the component. +~>

    + +{{T.h3 "Component Expression"}}

    -Calls of the form {{code "{{<component> <lua-table-expr>}}"}}. +You render/use the component using component expression, which looks like this: ~>

    +{{@ C.compSrc {file = stx, component = "use_component", include_tags = false} }} +

    -For example, {{code '{{greeting {name = "John"} }}'}} calls the component {{code "greeting"}} with {{code "name"}} set to {{code "John"}}, accessible from within the component as {{code "ctx.name"}}. +Here, we call the component {{code "greeting"}} and pass a table to the component, with {{code "name"}} set to {{code [["John"]]}}. Note that you can use *any* Lua expression, so long as it evaluates to a table. ~>

    -{{T.h3 "Line Continuation"}} +{{T.h2 "Line Continuation"}}

    Any line which starts with {{code "~>"}} (after optional indentation). ~>

    @@ -101,7 +91,7 @@ It may sound technical, but it is simple. A line continuation is just that, a co

    The following is a definition of the HTT grammar. {{T.h4 "How to read the grammar"}} -% @code +%%% local grammar_def = e.ul { e.li {grammarItem, { code = "// ...", text = "this is a comment"} }, e.li {grammarItem, { code = "<rule-name> =", text = "the name of a rule"} }, @@ -112,11 +102,11 @@ local grammar_def = e.ul { e.li {grammarItem, { code = "<rule>?", text = "0 or 1 repetitions of rule"} }, e.li {grammarItem, { code = "'...'", text = "anything in single quotes is a literal value"} }, } -% @end +%%% {{@ C.list grammar_def }} ~>

    {{T.h4 "Grammar"}} {{@ C.include {file = "examples/syntax/grammar.txt"} }} % -- end of document -% @end +%
    diff --git a/src/engine/tpl.zig b/src/engine/tpl.zig index 398cb45..52ab2e6 100644 --- a/src/engine/tpl.zig +++ b/src/engine/tpl.zig @@ -46,9 +46,23 @@ pub fn compile(l: *Lua) i32 { _ = l.pushString(e.content_type); _ = l.setTable(-3); }, - .directive_unknown => |du| { - _ = l.pushString("directive_tag"); - _ = l.pushString(du.tag); + .component_close_mismatch => |e| { + _ = l.pushString("open"); + _ = l.pushString(e.open); + _ = l.setTable(-3); + + _ = l.pushString("close"); + _ = l.pushString(e.close); + _ = l.setTable(-3); + }, + .component_start_missing => |e| { + _ = l.pushString("close"); + _ = l.pushString(e.close); + _ = l.setTable(-3); + }, + .component_close_missing => |e| { + _ = l.pushString("open"); + _ = l.pushString(e.open); _ = l.setTable(-3); }, else => {}, diff --git a/src/htt_typestubs.lua b/src/htt_typestubs.lua index 392c269..a83e96d 100644 --- a/src/htt_typestubs.lua +++ b/src/htt_typestubs.lua @@ -1,5 +1,11 @@ ---@meta +---Render component to file at `fpath`. +---@param component function the component to render +---@param fpath string relative path to file +---@param ctx table? context (arguments) to pass to the component +function render(component, fpath, ctx) end + ---HTT utility API ---@class htt ---@field str httStrModule nil @@ -11,6 +17,14 @@ ---@field json httJsonModule JSON de- serialization functions htt = {} +---@class htt.Component +htt.Component = {} + +---Check whether value is a HTT template component +---@param value any some value to check +---@return boolean True if a ComponentInstance, false otherwise +function htt.Component.is(value) end + ---@class httStrModule htt.str = {} diff --git a/src/prelude.lua b/src/prelude.lua index d2d0f59..b677d46 100644 --- a/src/prelude.lua +++ b/src/prelude.lua @@ -73,6 +73,24 @@ end -- Utilities -- ------------------------------------------------------------ +htt.Component = {} +htt.Component.__index = htt.Component + +function htt.Component.__call(self, ...) + return self._fn(...) +end + +function htt.Component.new(name, func) + return setmetatable({ + name = name, + _fn = func, + }, htt.Component) +end + +function htt.Component.is(val) + return getmetatable(val) == htt.Component +end + function htt.enum(tbl) local inst = {} local mt = { diff --git a/src/tpl/compiler.zig b/src/tpl/compiler.zig index 6060a89..1ac4062 100644 --- a/src/tpl/compiler.zig +++ b/src/tpl/compiler.zig @@ -71,18 +71,18 @@ fn write_code_impl(tpl_fpath: []const u8, buf: []u8, cur: usize, out: std.fs.Fil pub const CompileErrorType = enum { lex_err, illegal_toplevel_content, - directive_start_unmatched, - directive_end_unmatched, - directive_unknown, + component_close_mismatch, + component_start_missing, + component_close_missing, nested_component_error, }; pub const CompileErrorData = union(CompileErrorType) { lex_err: struct { reason: []const u8, state: []const u8 }, illegal_toplevel_content: struct { content_type: []const u8 }, - directive_start_unmatched: void, - directive_end_unmatched: void, - directive_unknown: struct { tag: []const u8 }, + component_close_mismatch: struct { open: []const u8, close: []const u8 }, + component_start_missing: struct { close: []const u8 }, + component_close_missing: struct { open: []const u8 }, nested_component_error: void, }; @@ -93,6 +93,10 @@ pub const CompileError = struct { type: CompileErrorData, }; +pub fn in_component(component_lvl: i32) bool { + return component_lvl > 0; +} + pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?CompileError { const tpl_file = try std.fs.cwd().openFile(tpl_fpath, .{ .mode = .read_only }); defer tpl_file.close(); @@ -120,12 +124,12 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com // TODO: maybe scan and determine the longest line first const cap = 2048; var buf = try a.alloc(u8, cap); - var dstack = std.ArrayList(Token).init(a); - defer dstack.deinit(); + var comp_stack = std.ArrayList(Token).init(a); + defer comp_stack.deinit(); defer a.free(buf); var cur: usize = 0; var code_indent: u32 = 0; - var in_component = false; + var component_lvl: i32 = 0; cur = memwrite(buf, cur, "-- Autogenerated from '"); cur = memwrite(buf, cur, tpl_fpath); @@ -142,7 +146,7 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com peek = lexer.nextToken(); switch (t.data) { .fresh_line => |fl| { - if (!in_component) { + if (!in_component(component_lvl)) { // This happens @ top-level, we ignore it continue; } @@ -152,7 +156,7 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com cur = memwrite(buf, cur, "')\n"); }, .line_continuation => { - if (!in_component) { + if (!in_component(component_lvl)) { return CompileError{ .reason = "content must be inside a component", .lineno = t.lineno, @@ -165,7 +169,7 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com cur = memwrite(buf, cur, "_T.cont()\n"); }, .text => |txt| { - if (!in_component) { + if (!in_component(component_lvl)) { if (txt.len == 0) { // this happens @ top-level, we ignore it continue; @@ -178,7 +182,7 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com .type = .{ .illegal_toplevel_content = .{ .content_type = "text" } }, }; } - if (!in_component and txt.len != 0) { + if (!in_component(component_lvl) and txt.len != 0) { continue; } // code indentation @@ -201,7 +205,7 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com cur = memwrite(buf, cur, "')\n"); }, .luaexpr => |expr| { - if (!in_component) { + if (!in_component(component_lvl)) { return CompileError{ .reason = "content must be inside a component", .lineno = t.lineno, @@ -215,7 +219,7 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com cur = memwrite(buf, cur, "))\n"); }, .render => |rargs| { - if (!in_component) { + if (!in_component(component_lvl)) { return CompileError{ .reason = "content must be inside a component", .lineno = t.lineno, @@ -242,69 +246,58 @@ pub fn compile(a: Allocator, tpl_fpath: []const u8, out_fpath: []const u8) !?Com cur = memwrite(buf, cur, lbl.lua); cur = memwrite(buf, cur, "\n"); }, - .directive => { - const d = t.data.directive; - if (!std.mem.eql(u8, "end", d.tag)) { - try dstack.append(t); - } else { - // END tag - if (dstack.popOrNull()) |dtok| { - if (std.mem.eql(u8, "component", dtok.data.directive.tag)) { - if (code_indent > 1) { - code_indent -= 2; - } - cur = memset_at(buf, cur, code_indent, ' '); - cur = memwrite(buf, cur, "end\n\n"); - - // alias components, this is what allows a render call to use - // the unqualified (local) name of a component - const cname = dtok.data.directive.args; - cur = memset_at(buf, cur, code_indent, ' '); - cur = memwrite(buf, cur, "local "); - cur = memwrite(buf, cur, cname); - cur = memwrite(buf, cur, " = M."); - cur = memwrite(buf, cur, cname); - cur = memwrite(buf, cur, "\n\n"); - - in_component = false; - } - } else { - return CompileError{ - .reason = "encountered `end`, but no directive tag is open", - .lineno = t.lineno, - .column = t.col, - .type = .{ .directive_start_unmatched = {} }, - }; + .component_begin => { + try comp_stack.append(t); + + cur = memset_at(buf, cur, code_indent, ' '); + cur = memwrite(buf, cur, "\nlocal "); + cur = memwrite(buf, cur, t.data.component_begin.name); // component name + cur = memwrite(buf, cur, " = htt.Component.new([["); + cur = memwrite(buf, cur, t.data.component_begin.name); // component name + cur = memwrite(buf, cur, "]], function(_T, ctx)\n"); + + code_indent += 2; + component_lvl += 1; + }, + .component_end => { + if (comp_stack.popOrNull()) |c_opn_tok| { + if (code_indent > 1) { + code_indent -= 2; } - continue; - } - if (std.mem.eql(u8, "component", d.tag)) { - if (in_component) { - // TODO: 100% illegal - std.debug.print("ERROR, cannot nest components!\n", .{}); + cur = memset_at(buf, cur, code_indent, ' '); + cur = memwrite(buf, cur, "end)\n\n"); + + const cname = c_opn_tok.data.component_begin.name; + if (!std.mem.eql(u8, cname, t.data.component_end.name)) { + return CompileError{ .reason = "unmatched component begin/end tags", .lineno = t.lineno, .column = t.col, .type = .{ .component_close_mismatch = .{ .open = cname, .close = t.data.component_end.name } } }; } - cur = memwrite(buf, cur, "function M."); - cur = memwrite(buf, cur, d.args); - cur = memwrite(buf, cur, "(_T, ctx)\n"); - code_indent += 2; - in_component = true; - } else if (std.mem.eql(u8, "code", d.tag)) {} else { + // alias components, this is what allows a render call to use + // the unqualified (local) name of a component + cur = memset_at(buf, cur, code_indent, ' '); + cur = memwrite(buf, cur, "M."); + cur = memwrite(buf, cur, cname); + cur = memwrite(buf, cur, " = "); + cur = memwrite(buf, cur, cname); + cur = memwrite(buf, cur, "\n"); + + component_lvl -= 1; + } else { return CompileError{ - .reason = "unknown directive type", + .reason = "encountered component close tag, but no component is open", .lineno = t.lineno, .column = t.col, - .type = .{ .directive_unknown = .{ .tag = d.tag } }, + .type = .{ .component_start_missing = .{ .close = t.data.component_end.name } }, }; } }, .eof => { - if (dstack.popOrNull()) |dtok| { + if (comp_stack.popOrNull()) |ctok| { return CompileError{ - .reason = "directive not closed", - .lineno = dtok.lineno, - .column = dtok.col, - .type = .{ .directive_end_unmatched = {} }, + .reason = "component not closed", + .lineno = ctok.lineno, + .column = ctok.col, + .type = .{ .component_close_missing = .{ .open = ctok.data.component_begin.name } }, }; } break; diff --git a/src/tpl/lexer.zig b/src/tpl/lexer.zig index afb774b..52ebed7 100644 --- a/src/tpl/lexer.zig +++ b/src/tpl/lexer.zig @@ -29,13 +29,14 @@ const CH_EOF: u8 = DC1; // TODO: code to lex until '\n\s*% @end' -- emit single block of lua code pub const LexErr = struct { - const DirExpectedAt = "Expected whitespace or '@' sign to signify start of directive"; - const DirectiveTagMissing = "Directive is missing its tag (i.e. @component, @code, @end)"; - const ExpectedWs = "Expected whitespace"; - const UnexpectedNewline = "Unexpected newline"; - const UnexpectedEof = "Unexpected end of file"; - const ComponentIdMissing = "Render expression should start with a component identifier"; - const Illegal = "Illegal state, lexing aborted"; + pub const InvalidComponentTag = "component tags must be a name surrounded by '<' and '>'"; + pub const ComponentNameMissing = "you must provide the component name"; + pub const ExpectedWs = "Expected whitespace"; + pub const UnexpectedNewline = "Unexpected newline"; + pub const UnexpectedEof = "Unexpected end of file"; + pub const ComponentIdMissing = "Render expression should start with a component identifier"; + pub const ExpectedNl = "Expected end-of-line"; + pub const Illegal = "Illegal state, lexing aborted"; }; // NOTE: IDEA: std.unicode.utf8ByteSequenceLength(b: u8) @@ -90,15 +91,8 @@ inline fn cmp4(comptime T: type, a: []const T, b: []const T, b_offset: u64) bool return (a[0] == b[b_offset] and a[1] == b[b_offset + 1] and a[2] == b[b_offset + 2] and a[3] == b[b_offset + 3]); } -inline fn peekMatch(input: []const u8, pos: u64, comptime slice: []const u8) bool { - const cmpFn = comptime switch (slice.len) { - 1 => cmp1, - 2 => cmp2, - 3 => cmp3, - 4 => cmp4, - else => @compileError("no support for slices of this length"), - }; - return ((pos + 1 + slice.len) < input.len and cmpFn(u8, slice, input, pos + 1)); +inline fn peekMatch(input: []const u8, pos: u64, slice: []const u8) bool { + return (pos + slice.len) <= input.len and std.mem.eql(u8, input[pos .. pos + slice.len], slice); } pub const Lexer = struct { @@ -122,7 +116,7 @@ pub const Lexer = struct { fn consumeNewline(self: *@This(), pos: u64) void { if (self.input[pos] != '\n') { - std.debug.panic("expected to be at newline\n (pos: {}, ch {})", .{ pos, self.input[pos] }); + std.debug.panic("Expected to be at newline\n\tPos: {}\n\tLine: {}\n\tCh: {c}\n\tRest:\n\n{s}\n---\n", .{ pos, self.lineno, self.input[pos], self.input[pos..self.input.len] }); } self.pos = pos + 1; // consume \n self.lineno += 1; @@ -230,75 +224,66 @@ pub const Lexer = struct { return t; } - fn _lexDirective(self: *@This()) tok.Token { + fn lexComponentLine(self: *@This()) tok.Token { + // precond: we've already consumed '@' const input = self.input; var pos = self.pos; - // position of the start of the directive tag + // '@' consumed, position is at the start of the 'begin'/'end' tag if (pos == input.len) { return self.setIllegalState(pos, LexErr.UnexpectedEof); } else if (input[pos] == '\n') { return self.setIllegalState(pos, LexErr.UnexpectedNewline); } - const dtag_start_pos = pos; + const close: bool = input[pos] == '/'; + if (close) { + pos += 1; + } + + const name_start_pos = pos; while (pos < input.len) : (pos += 1) { const ch = input[pos]; if (chIsLetter(ch) or chIsDigit(ch) or ch == '_') {} else { break; } } - const dtag_end_pos = pos; - if (dtag_start_pos == dtag_end_pos) { - return self.setIllegalState(dtag_start_pos, LexErr.DirectiveTagMissing); - } + const name_end_pos = pos; - var t = self.newToken(); - t.data = .{ .directive = .{ .tag = input[dtag_start_pos..dtag_end_pos], .args = "" } }; + if (name_start_pos == name_end_pos) { + return self.setIllegalState(name_start_pos, LexErr.ComponentNameMissing); + } if (pos == input.len) { - self.pos = pos; - self.state = LexState.eof; - return t; - } else if (input[pos] == '\n') { - self.pos = pos; - self.state = .line_end; - return t; + return self.setIllegalState(pos, LexErr.UnexpectedEof); } - // ensure what followed the tag is a whitespace character - if (!chIsWhitespace(input[pos])) { - return self.setIllegalState(pos, LexErr.ExpectedWs); + if (input[pos] != '>') { + return self.setIllegalState(pos, LexErr.InvalidComponentTag); } - // skip any additional whitespace + pos += 1; // skip past the '>' + + // consume any trailing whitespace pos = skipWhitespace(input, pos); - const darg_start_pos = pos; - while (pos < input.len) : (pos += 1) { - if (input[pos] == '\n') { - break; - } + var t = self.newToken(); + if (close) { + t.data = .{ .component_end = .{ .name = input[name_start_pos..name_end_pos] } }; + } else { + t.data = .{ .component_begin = .{ .name = input[name_start_pos..name_end_pos] } }; } self.pos = pos; if (pos == input.len) { - self.state = .eof; - } else if (input[pos] == '\n') { + if (close) { + self.state = .eof; + } else { + return self.setIllegalState(pos, LexErr.UnexpectedEof); + } + } else { + // already enforced this above self.state = .line_end; } - - t.data.directive.args = input[darg_start_pos..pos]; - return t; - } - - fn lexDirective(self: *@This()) tok.Token { - const t = self._lexDirective(); - if (std.mem.eql(u8, t.data.directive.tag, "code")) { - // skipping past .line_end state, so must register newline - self.consumeNewline(self.pos); - self.state = LexState.lua_blk; - } - // TODO: could check more here, such as if not @end at EOF, then ERROR return t; } @@ -306,12 +291,6 @@ pub const Lexer = struct { const input = self.input; var pos = self.pos; - if (pos == input.len) { - return self.setIllegalState(pos, LexErr.UnexpectedEof); - } else if (input[pos] == '\n') { - return self.setIllegalState(pos, LexErr.UnexpectedNewline); - } - // capture characters until newline verbatim, assume regular lua while (pos < input.len) { if (input[pos] == '\n') { @@ -605,25 +584,23 @@ pub const Lexer = struct { return t; }, '%' => { - const pct_pos = pos; - // skip WS after '%' - pos = skipWhitespace(input, pos + 1); - - // check if we have the start of a directive line - if (input.len == pos or input[pos] != '@') { - // will probably be an illegal lua line, but that possibility - // should be allowed for, generally. - pos = pct_pos; + if (peekMatch(self.input, pos, "%%%")) { + pos += "%%%".len; + // lua block end + var t = self.newToken(); + t.data = .{ .luablock_delim = {} }; + self.pos = skipWhitespace(input, pos); + if (self.pos == input.len) { + self.state = .eof; + } else { + self.state = .line_end; // expect a newline after + } + return t; + } else { + // will probably be an illegal Lua line, but that possibility should be allowed for + pos += 1; // consume the '%' break; } - pos += 1; // consume '@' - - self.pos = pos; // update so lexDirective starts at proper offset - const t = self._lexDirective(); - if (!std.mem.eql(u8, t.data.directive.tag, "end")) { - return self.setIllegalState(pos, LexErr.Illegal); // TODO: describe issue. Require end directive - } - return t; }, else => { break; @@ -691,19 +668,24 @@ pub const Lexer = struct { }, '%' => { const indent = input[self.pos..pos]; + if (peekMatch(input, pos, "%%%")) { + // Lua block delimiter + pos += "%%%".len; + pos = skipWhitespace(input, pos); + var t = self.newToken(); + t.data = .{ .luablock_delim = {} }; + // skipping past .line_end state, so must register newline + self.consumeNewline(pos); // assigns pos also + self.state = .lua_blk; + return t; + } // skip '%' (+1) and then any ws after pos = skipWhitespace(input, pos + 1); - if (input[pos] == '\n') { - return self.setIllegalState(pos, LexErr.UnexpectedNewline); - } else if (pos == input.len) { - return self.setIllegalState(pos, LexErr.UnexpectedEof); - } - - if (input[pos] == '@') { - self.pos = pos + 1; // discard other chrs, skip '@' - return self.lexDirective(); + if (input[pos] == '<') { + self.pos = pos + 1; // discard other chrs, skip '<' + return self.lexComponentLine(); } else { self.pos = pos; // skip leading ws up to lua code return self.lexLuaLine(indent); diff --git a/src/tpl/test_lexer.zig b/src/tpl/test_lexer.zig index 1af5328..6d59966 100644 --- a/src/tpl/test_lexer.zig +++ b/src/tpl/test_lexer.zig @@ -7,7 +7,7 @@ fn ppToken(lbl: []const u8, tok: Token) void { std.debug.print("{s} Token {{\n\tfpath: \"{s}\",\n\tlineno: {},\n\tcol: {},\n\t: {s},\n\tdata: ", .{ lbl, tok.fpath, tok.lineno, tok.col, @tagName(tok.data) }); switch (tok.data) { .illegal => { - std.debug.print("illegal(reason: {s}, state: {s})\n", .{ tok.data.illegal.reason, @tagName(tok.data.illegal.state) }); + std.debug.print("reason: {s}, state: {s}\n", .{ tok.data.illegal.reason, @tagName(tok.data.illegal.state) }); }, .fresh_line => { std.debug.print("\"{s}\"\n", .{tok.data.fresh_line.indent}); @@ -15,12 +15,18 @@ fn ppToken(lbl: []const u8, tok: Token) void { .text => { std.debug.print("\"{s}\"\n", .{tok.data.text}); }, - .directive => { - std.debug.print("{{\n\t\ttag: \"{s}\",\n\t\targs: \"{s}\"\n\t}}\n", .{ tok.data.directive.tag, tok.data.directive.args }); + .component_begin => { + std.debug.print("<{s}>\n", .{tok.data.component_begin.name}); + }, + .component_end => { + std.debug.print("\n", .{tok.data.component_end.name}); }, .lualine => { std.debug.print("\"{s}{s}\"\n", .{ tok.data.lualine.indent, tok.data.lualine.lua }); }, + .luablock_delim => { + std.debug.print("%%%\n", .{}); + }, .luablock_line => { std.debug.print("\"{s}{s}\"\n", .{ tok.data.luablock_line.indent, tok.data.luablock_line.lua }); }, @@ -36,6 +42,7 @@ fn ppToken(lbl: []const u8, tok: Token) void { } std.debug.print("}}\n", .{}); } + fn expectToken_(exp: Token, actual: Token) !void { if (!std.mem.eql(u8, exp.fpath, actual.fpath)) { std.debug.print("!fpath; expected '{s}', got '{s}'\n", .{ exp.fpath, actual.fpath }); @@ -57,7 +64,18 @@ fn expectToken_(exp: Token, actual: Token) !void { try std.testing.expect(false); } - try std.testing.expectEqualDeep(exp.data, actual.data); + switch (actual.data) { + .illegal => { + // NOTE: deliberately ignoring the .state variable, this is an implementation/debugging detail + if (!std.mem.eql(u8, actual.data.illegal.reason, exp.data.illegal.reason)) { + std.debug.print("Unexpected Illegal state reason, Expected: {s}, Got: {s}\n", .{ exp.data.illegal.reason, actual.data.illegal.reason }); + try std.testing.expect(false); + } + }, + else => { + try std.testing.expectEqualDeep(exp.data, actual.data); + }, + } } fn expectToken(exp: Token, actual: Token) !void { @@ -84,6 +102,9 @@ fn ppTokens(max: u32, l: *Lexer) !void { while (!l.eof() and ndx < max) : (ndx += 1) { ppToken("token", l.nextToken()); } + if (l.eof() and ndx < max) { + return; + } return error.OutOfMemory; } @@ -266,6 +287,23 @@ test "lualine indent" { }, &lex); } +// TODO: *could* refine output to not emit the empty lua line at all. +// This would not impact the template's behavior. +test "lualine, allow empty lua lines" { + const fpath = "x.tpl"; + var lex = Lexer.init(fpath, + \\% + \\% + \\ + ); + + try expectTokens(&[_]Token{ + .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .lualine = .{ .indent = "", .lua = "" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .lualine = .{ .indent = "", .lua = "" } } }, + .{ .fpath = fpath, .lineno = 3, .col = 0, .data = .{ .eof = {} } }, + }, &lex); +} + test "text, luaexpr double-quoted }}" { const fpath = "y.tpl"; var lex = Lexer.init(fpath, @@ -364,19 +402,19 @@ test "render, line w expr" { test "code block" { const fpath = "y.tpl"; var lex = Lexer.init(fpath, - \\% @code + \\%%% \\for ndx, val in ipairs({1, 2, 3}) do \\ print(tostring(ndx) .. ": " .. tostring(val)) - \\done - \\% @end + \\end + \\%%% ); //try ppTokens(20, &lex); try expectTokens(&[_]Token{ - .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .directive = .{ .tag = "code", .args = "" } } }, + .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .luablock_delim = {} } }, .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .luablock_line = .{ .indent = "", .lua = "for ndx, val in ipairs({1, 2, 3}) do" } } }, .{ .fpath = fpath, .lineno = 3, .col = 0, .data = .{ .luablock_line = .{ .indent = " ", .lua = "print(tostring(ndx) .. \": \" .. tostring(val))" } } }, - .{ .fpath = fpath, .lineno = 4, .col = 0, .data = .{ .luablock_line = .{ .indent = "", .lua = "done" } } }, - .{ .fpath = fpath, .lineno = 5, .col = 0, .data = .{ .directive = .{ .tag = "end", .args = "" } } }, + .{ .fpath = fpath, .lineno = 4, .col = 0, .data = .{ .luablock_line = .{ .indent = "", .lua = "end" } } }, + .{ .fpath = fpath, .lineno = 5, .col = 0, .data = .{ .luablock_delim = {} } }, .{ .fpath = fpath, .lineno = 5, .col = 0, .data = .{ .eof = {} } }, }, &lex); } @@ -411,24 +449,39 @@ test "code block" { test "component start -- sudden EOF" { const fpath = "x.tpl"; var lex = Lexer.init(fpath, - \\% @component thing + \\% ); try expectTokens(&[_]Token{ - Token{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .directive = .{ .tag = "component", .args = "thing" } } }, + Token{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .illegal = .{ .reason = lexer.LexErr.UnexpectedEof, .state = .toplevel } } }, + }, &lex); +} + +test "component mismatched start/end" { + // This seems strange, but this issue is handled at the compiler-level + const fpath = "x.tpl"; + var lex = Lexer.init(fpath, + \\% + \\% + ); + + try expectTokens(&[_]Token{ + .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .component_begin = .{ .name = "foo" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .component_end = .{ .name = "bar" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .eof = {} } }, }, &lex); } test "component start, then end (immediate EOF)" { const fpath = "x.tpl"; var lex = Lexer.init(fpath, - \\% @component thing - \\% @end + \\% + \\% ); try expectTokens(&[_]Token{ - .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .directive = .{ .tag = "component", .args = "thing" } } }, - .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .directive = .{ .tag = "end", .args = "" } } }, + .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .component_begin = .{ .name = "thing" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .component_end = .{ .name = "thing" } } }, .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .eof = {} } }, }, &lex); } @@ -436,19 +489,38 @@ test "component start, then end (immediate EOF)" { test "component start, then end (newline)" { const fpath = "x.tpl"; var lex = Lexer.init(fpath, - \\% @component thing - \\% @end + \\% + \\% \\ ); //try ppTokens(10, &lex); try expectTokens(&[_]Token{ - .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .directive = .{ .tag = "component", .args = "thing" } } }, - .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .directive = .{ .tag = "end", .args = "" } } }, + .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .component_begin = .{ .name = "thing" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .component_end = .{ .name = "thing" } } }, .{ .fpath = fpath, .lineno = 3, .col = 0, .data = .{ .eof = {} } }, }, &lex); } +test "nested components" { + const fpath = "x.tpl"; + var lex = Lexer.init(fpath, + \\% + \\% + \\% + \\% + \\ + ); + + try expectTokens(&[_]Token{ + .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .component_begin = .{ .name = "foo" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .component_begin = .{ .name = "bar" } } }, + .{ .fpath = fpath, .lineno = 3, .col = 0, .data = .{ .component_end = .{ .name = "bar" } } }, + .{ .fpath = fpath, .lineno = 4, .col = 0, .data = .{ .component_end = .{ .name = "foo" } } }, + .{ .fpath = fpath, .lineno = 5, .col = 0, .data = .{ .eof = {} } }, + }, &lex); +} + // // test "lua block, single-quote escape " { // // // don't interpret '%>' (lua block close) when in escaped context // // const fpath = "x.tpl"; @@ -535,18 +607,18 @@ test "prog1" { const fpath = "x.tpl"; var lex = Lexer.init(fpath, \\% require "hello" - \\% @component struct + \\% \\typedef struct { \\%for typ, lbl in ctx.members do \\{{ typ }} {{ lbl}}; \\%end \\} - \\% @end + \\% ); try expectTokens(&[_]Token{ .{ .fpath = fpath, .lineno = 1, .col = 0, .data = .{ .lualine = .{ .indent = "", .lua = "require \"hello\"" } } }, - .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .directive = .{ .tag = "component", .args = "struct" } } }, + .{ .fpath = fpath, .lineno = 2, .col = 0, .data = .{ .component_begin = .{ .name = "struct" } } }, .{ .fpath = fpath, .lineno = 3, .col = 0, .data = .{ .fresh_line = .{ .indent = "" } } }, .{ .fpath = fpath, .lineno = 3, .col = 0, .data = .{ .text = "typedef struct {" } }, .{ .fpath = fpath, .lineno = 4, .col = 0, .data = .{ .lualine = .{ .indent = "", .lua = "for typ, lbl in ctx.members do" } } }, @@ -562,7 +634,7 @@ test "prog1" { .{ .fpath = fpath, .lineno = 6, .col = 0, .data = .{ .lualine = .{ .indent = "", .lua = "end" } } }, .{ .fpath = fpath, .lineno = 7, .col = 0, .data = .{ .fresh_line = .{ .indent = "" } } }, .{ .fpath = fpath, .lineno = 7, .col = 0, .data = .{ .text = "}" } }, - .{ .fpath = fpath, .lineno = 8, .col = 0, .data = .{ .directive = .{ .tag = "end", .args = "" } } }, + .{ .fpath = fpath, .lineno = 8, .col = 0, .data = .{ .component_end = .{ .name = "struct" } } }, .{ .fpath = fpath, .lineno = 8, .col = 0, .data = .{ .eof = {} } }, }, &lex); } diff --git a/src/tpl/token.zig b/src/tpl/token.zig index 21dbd88..837e91b 100644 --- a/src/tpl/token.zig +++ b/src/tpl/token.zig @@ -4,8 +4,10 @@ pub const TokenType = enum { fresh_line, line_continuation, text, - directive, + component_begin, + component_end, lualine, + luablock_delim, luablock_line, luaexpr_open, luaexpr, @@ -38,8 +40,10 @@ pub const TokenData = union(TokenType) { fresh_line: struct { indent: []const u8 }, line_continuation: void, text: []const u8, - directive: struct { tag: []const u8, args: []const u8 }, + component_begin: struct { name: []const u8 }, + component_end: struct { name: []const u8 }, lualine: struct { lua: []const u8, indent: []const u8 }, + luablock_delim: void, luablock_line: struct { lua: []const u8, indent: []const u8 }, luaexpr_open: void, luaexpr: []const u8, diff --git a/tests/tpl/expected/03_render/test_nested_components/outer1 b/tests/tpl/expected/03_render/test_nested_components/outer1 new file mode 100644 index 0000000..5012e51 --- /dev/null +++ b/tests/tpl/expected/03_render/test_nested_components/outer1 @@ -0,0 +1,5 @@ +o first line +o second line +i first line + i second line +o third line \ No newline at end of file diff --git a/tests/tpl/expected/03_render/test_nested_components/outer2 b/tests/tpl/expected/03_render/test_nested_components/outer2 new file mode 100644 index 0000000..5012e51 --- /dev/null +++ b/tests/tpl/expected/03_render/test_nested_components/outer2 @@ -0,0 +1,5 @@ +o first line +o second line +i first line + i second line +o third line \ No newline at end of file diff --git a/tests/tpl/expected/03_render/test_nested_components/outer3 b/tests/tpl/expected/03_render/test_nested_components/outer3 new file mode 100644 index 0000000..0fa9440 --- /dev/null +++ b/tests/tpl/expected/03_render/test_nested_components/outer3 @@ -0,0 +1,5 @@ +o first line +o second line + i first line + i second line +o third line \ No newline at end of file diff --git a/tests/tpl/expected/03_render/test_nested_components/outer4 b/tests/tpl/expected/03_render/test_nested_components/outer4 new file mode 100644 index 0000000..30e33c2 --- /dev/null +++ b/tests/tpl/expected/03_render/test_nested_components/outer4 @@ -0,0 +1,5 @@ +o first line +o second line +i name=Jane, profession=Plumber + i second line +o third line \ No newline at end of file diff --git a/tests/tpl/expected/06_component_api/test_component_is/out b/tests/tpl/expected/06_component_api/test_component_is/out new file mode 100644 index 0000000..4383f01 --- /dev/null +++ b/tests/tpl/expected/06_component_api/test_component_is/out @@ -0,0 +1,6 @@ +1: outer#1>: true +3: inner#1?: true +5: comp from ctx?: true +6: a fn? false +6: a number? false +6: a string? false \ No newline at end of file diff --git a/tests/tpl/expected/06_component_api/test_component_name/out b/tests/tpl/expected/06_component_api/test_component_name/out new file mode 100644 index 0000000..1a91724 --- /dev/null +++ b/tests/tpl/expected/06_component_api/test_component_name/out @@ -0,0 +1,5 @@ +1: outer#1: outer_one +2: outer#2: outer_two +3: inner#1: inner_one +4: inner#2: inner_two +5: from ctx#1: component_passed_by_ctx \ No newline at end of file diff --git a/tests/tpl/inputs/01_render/hello.htt b/tests/tpl/inputs/01_render/hello.htt index 4271e2b..44fe5c2 100644 --- a/tests/tpl/inputs/01_render/hello.htt +++ b/tests/tpl/inputs/01_render/hello.htt @@ -1,8 +1,8 @@ -% @component Main +%
    hello world! -% @end +%
    -% @component LuaExpr +% 2 + 2 is {{2 + 2}} -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/02_basics/context.htt b/tests/tpl/inputs/02_basics/context.htt index 6eca256..50e858c 100644 --- a/tests/tpl/inputs/02_basics/context.htt +++ b/tests/tpl/inputs/02_basics/context.htt @@ -1,9 +1,9 @@ -% @component Child +% Hello {{ctx.name or ""}} -% @end +% % -- the following also demonstrates the render call -% @component Parent +% Parent says: {{@ Child ctx }} -% @end +% diff --git a/tests/tpl/inputs/02_basics/exprs.htt b/tests/tpl/inputs/02_basics/exprs.htt index 28a5060..1ad232b 100644 --- a/tests/tpl/inputs/02_basics/exprs.htt +++ b/tests/tpl/inputs/02_basics/exprs.htt @@ -1,4 +1,4 @@ -% @component LuaExpr +% 2 + 2 is {{2 + 2}}! -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/02_basics/line_continuation.htt b/tests/tpl/inputs/02_basics/line_continuation.htt index 4e7751e..1c73822 100644 --- a/tests/tpl/inputs/02_basics/line_continuation.htt +++ b/tests/tpl/inputs/02_basics/line_continuation.htt @@ -1,4 +1,4 @@ -% @component Simple +% Hello ~>World! @@ -10,12 +10,12 @@ Hello Hello ~> World! -% @end +% -% @component Array +% [ 1 % for i = 2, 3 do ~>, {{i}} % end ~> ] -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/02_basics/lua_blocks.htt b/tests/tpl/inputs/02_basics/lua_blocks.htt index 26b88b1..ae36082 100644 --- a/tests/tpl/inputs/02_basics/lua_blocks.htt +++ b/tests/tpl/inputs/02_basics/lua_blocks.htt @@ -1,13 +1,13 @@ -% @code +%%% local name = "Jane" local function greet(name) return string.format("Hello, %s!", name) end -% @end +%%% -% @component Main -% @code +%
    +%%% name = "Anna" -% @end +%%% {{ greet(name) }}! -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/tests/tpl/inputs/02_basics/lua_lines.htt b/tests/tpl/inputs/02_basics/lua_lines.htt index fa50c70..653cbb2 100644 --- a/tests/tpl/inputs/02_basics/lua_lines.htt +++ b/tests/tpl/inputs/02_basics/lua_lines.htt @@ -1,18 +1,18 @@ -% @component Main +%
    % local name = "John" Hello, {{name}}! % name = "Jane" Hello, {{name}}! -% @end +%
    -% @component LuaLineLoop +% % for i = 1, 3 do {{i}} missisipi! % end Done! -% @end +% -% @component ConditionalRender +% % if ctx.show == true then I will show you % elseif ctx.show == false then @@ -20,4 +20,4 @@ I will NOT show you % else Hmm, I am unsure of you % end -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/02_basics/render_expr.htt b/tests/tpl/inputs/02_basics/render_expr.htt index 61f4adf..79cee8d 100644 --- a/tests/tpl/inputs/02_basics/render_expr.htt +++ b/tests/tpl/inputs/02_basics/render_expr.htt @@ -1,10 +1,10 @@ % -- Render component `Child` from `Parent`. % -- % -- Note, order matters, must define Child before Parent (ordinarily, see advanced) -% @component Child +% Hello, from child -% @end +% -% @component Parent +% {{@ Child {} }} -% @end +% diff --git a/tests/tpl/inputs/02_basics/tpl_with_luafile.htt b/tests/tpl/inputs/02_basics/tpl_with_luafile.htt index 2950c28..4649856 100644 --- a/tests/tpl/inputs/02_basics/tpl_with_luafile.htt +++ b/tests/tpl/inputs/02_basics/tpl_with_luafile.htt @@ -1,4 +1,4 @@ % -- HINT: look in tpl_with_lua.htt.lua -% @component Main +%
    Hello {{name}} -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/tests/tpl/inputs/03_render/ctx.htt b/tests/tpl/inputs/03_render/ctx.htt index 5962a03..0132f67 100644 --- a/tests/tpl/inputs/03_render/ctx.htt +++ b/tests/tpl/inputs/03_render/ctx.htt @@ -1,15 +1,15 @@ -% @component Top +% {{ ctx.name or "unset" }} -% @end +% -% @component Child +% Hello, my name is {{ctx.name}}, I am a {{ctx.profession}} by trade. -% @end +% -% @component CtxPassthrough +% {{@ Child ctx }} -% @end +% -% @component CtxFromComponent +% {{@ Child {name = "Jane", profession = "painter"} }} -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/03_render/hoc.htt b/tests/tpl/inputs/03_render/hoc.htt index fbde83c..117920d 100644 --- a/tests/tpl/inputs/03_render/hoc.htt +++ b/tests/tpl/inputs/03_render/hoc.htt @@ -1,9 +1,9 @@ -% @component HocComponent +% And now. {{@ ctx.child {heading = ctx.text} }} Done. -% @end +% -% @component MdH1 +% # {{ctx.heading}} -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/03_render/nested_components.htt b/tests/tpl/inputs/03_render/nested_components.htt new file mode 100644 index 0000000..3536f58 --- /dev/null +++ b/tests/tpl/inputs/03_render/nested_components.htt @@ -0,0 +1,47 @@ + +% +o first line +% +i first line + i second line +% +o second line +{{@ inner {} }} +o third line +% + +% +o first line + % +i first line + i second line + % +o second line +{{@ inner {} }} +o third line +% + +% +o first line +% + i first line + i second line +% +o second line +{{@ inner {} }} +o third line +% + + +% +o first line +% ctx.name = "John" +% local profession = "Plumber" +% +i name={{ctx.name}}, profession={{profession}} + i second line +% +o second line +{{@ inner {name = "Jane"} }} +o third line +% \ No newline at end of file diff --git a/tests/tpl/inputs/03_render/test_nested_components.lua b/tests/tpl/inputs/03_render/test_nested_components.lua new file mode 100644 index 0000000..e2d635a --- /dev/null +++ b/tests/tpl/inputs/03_render/test_nested_components.lua @@ -0,0 +1,7 @@ + +local tpl = require("//nested_components.htt") + +render(tpl.outer1, "outer1", {}) +render(tpl.outer2, "outer2", {}) +render(tpl.outer3, "outer3", {}) +render(tpl.outer4, "outer4", {}) diff --git a/tests/tpl/inputs/04_formatting/indentation.htt b/tests/tpl/inputs/04_formatting/indentation.htt index 1037576..945ed55 100644 --- a/tests/tpl/inputs/04_formatting/indentation.htt +++ b/tests/tpl/inputs/04_formatting/indentation.htt @@ -1,46 +1,46 @@ -% @component LeafFlat1 +% LeafFlat1#1 -% @end +% -% @component LeafFlat2 +% LeafFlat2#1 LeafFlat2#2 -% @end +% -% @component LeafIndent1 +% LeafIndent1#1 LeafIndent1#2 -% @end +% -% @component LeafIndent2 +% LeafIndent2#1 LeafIndent2#2 -% @end +% -% @component LeafIndentBoth +% LeafIndentBoth#1 LeafIndentBoth#2 -% @end +% -% @component NodeFlat1 +% {{@ ctx.child {} }} -% @end +% -% @component NodeFlat2 +% . {{@ ctx.child {} }} -% @end +% -% @component NodeIndent1 +% {{@ ctx.child {} }} -% @end +% -% @component NodeIndent2 +% . {{@ ctx.child {} }} -% @end +% -% @component NodeIndentBoth +% . {{@ ctx.child {} }} -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/04_formatting/inline_components.htt b/tests/tpl/inputs/04_formatting/inline_components.htt index 3cfc2c3..5f1ea19 100644 --- a/tests/tpl/inputs/04_formatting/inline_components.htt +++ b/tests/tpl/inputs/04_formatting/inline_components.htt @@ -1,35 +1,35 @@ -% @component InnerFlat1 +% InnerFlat1#1 -% @end +% -% @component InnerFlat2 +% InnerFlat2#1 InnerFlat2#2 -% @end +% -% @component InnerIndent1 +% InnerIndent1#1 -% @end +% -% @component InnerIndent2 +% InnerIndent2#1 InnerIndent2#2 -% @end +% -% @component OuterFlat1 +% OF1({{@ ctx.child {} }}) -% @end +% -% @component OuterFlat2 +% OF2({{@ ctx.child {} }}) OF2({{@ ctx.child {} }}) -% @end +% -% @component OuterIndent1 +% OI1({{@ ctx.child {} }}) -% @end +% -% @component OuterIndent2 +% OI2({{@ ctx.child {} }}) OI2({{@ ctx.child {} }}) -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/04_formatting/nl_tests.htt b/tests/tpl/inputs/04_formatting/nl_tests.htt index d3bf387..550463f 100644 --- a/tests/tpl/inputs/04_formatting/nl_tests.htt +++ b/tests/tpl/inputs/04_formatting/nl_tests.htt @@ -1,50 +1,50 @@ -% @component L1 +% l1 -% @end +% -% @component L2 +% l2 -% @end +% -% @component T1 +% t1 -% @end +% -% @component T2 +% t2 -% @end +% -% @component L1T1 +% l1t1 -% @end +% -% @component L2T2 +% l2t2 -% @end +% -% @component Outer +% --- {{@ ctx.child {} }} --- -% @end +% -% @component OuterInline1 +% ---{{@ ctx.child {} }}--- -% @end +% -% @component OuterInline2 +% ---{{@ ctx.child {} }}--- -% @end \ No newline at end of file +% \ No newline at end of file diff --git a/tests/tpl/inputs/05_require/foo/bar/nested.htt b/tests/tpl/inputs/05_require/foo/bar/nested.htt index ba58113..e46b6b7 100644 --- a/tests/tpl/inputs/05_require/foo/bar/nested.htt +++ b/tests/tpl/inputs/05_require/foo/bar/nested.htt @@ -1,4 +1,4 @@ -% @component Main +%
    hello, from nested -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/tests/tpl/inputs/05_require/same_dir.htt b/tests/tpl/inputs/05_require/same_dir.htt index d5fc0c9..2115243 100644 --- a/tests/tpl/inputs/05_require/same_dir.htt +++ b/tests/tpl/inputs/05_require/same_dir.htt @@ -1,4 +1,4 @@ -% @component Main +%
    hello, from same dir -% @end \ No newline at end of file +%
    \ No newline at end of file diff --git a/tests/tpl/inputs/05_require/test_htt_require.lua b/tests/tpl/inputs/05_require/test_htt_require.lua index 9dfa06e..8636b16 100644 --- a/tests/tpl/inputs/05_require/test_htt_require.lua +++ b/tests/tpl/inputs/05_require/test_htt_require.lua @@ -1,4 +1,2 @@ - - render(require("//same_dir.htt").Main, "out.simple", {}) render(require("//foo/bar/nested.htt").Main, "out.nested", {}) \ No newline at end of file diff --git a/tests/tpl/inputs/06_component_api/component_is.htt b/tests/tpl/inputs/06_component_api/component_is.htt new file mode 100644 index 0000000..5b75750 --- /dev/null +++ b/tests/tpl/inputs/06_component_api/component_is.htt @@ -0,0 +1,28 @@ + +%%% +local function hello() return nil end +local x = 1 +local y = "hello" +%%% + + +% +% + +% + % + % +1: outer#1>: {{htt.Component.is(outer_one)}} +3: inner#1?: {{htt.Component.is(inner_one)}} +5: comp from ctx?: {{htt.Component.is(ctx.component)}} +6: a fn? {{htt.Component.is(hello)}} +6: a number? {{htt.Component.is(x)}} +6: a string? {{htt.Component.is(y)}} +% + +% +% + +%
    +{{@ component_name {component = component_passed_by_ctx} }} +%
    \ No newline at end of file diff --git a/tests/tpl/inputs/06_component_api/component_name.htt b/tests/tpl/inputs/06_component_api/component_name.htt new file mode 100644 index 0000000..005b050 --- /dev/null +++ b/tests/tpl/inputs/06_component_api/component_name.htt @@ -0,0 +1,25 @@ + +% +% + +% +% + +% + % + % + % + % +1: outer#1: {{outer_one.name}} +2: outer#2: {{outer_two.name}} +3: inner#1: {{inner_one.name}} +4: inner#2: {{inner_two.name}} +5: from ctx#1: {{ctx.component.name}} +% + +% +% + +%
    +{{@ component_name {component = component_passed_by_ctx} }} +%
    \ No newline at end of file diff --git a/tests/tpl/inputs/06_component_api/test_component_is.lua b/tests/tpl/inputs/06_component_api/test_component_is.lua new file mode 100644 index 0000000..3a131a0 --- /dev/null +++ b/tests/tpl/inputs/06_component_api/test_component_is.lua @@ -0,0 +1 @@ +render(require("//component_is.htt").main, "out", {}) diff --git a/tests/tpl/inputs/06_component_api/test_component_name.lua b/tests/tpl/inputs/06_component_api/test_component_name.lua new file mode 100644 index 0000000..522724a --- /dev/null +++ b/tests/tpl/inputs/06_component_api/test_component_name.lua @@ -0,0 +1 @@ +render(require("//component_name.htt").main, "out", {})