Module: Lilac::CLI::SFC
- Defined in:
- lib/lilac/cli/build/sfc.rb
Overview
Parses a .lil single-file component into its constituent parts:
* one or more `<template>` blocks, optionally named via
`data-template="..."` (a standard HTML5 data attribute, same as
what the Lilac runtime expects for clone-targets)
* one or more `<script type="text/ruby">` blocks
The format is HTML5-valid: any conformant HTML parser (browser,
nokogiri, gammo) can build a DOM from a .lil file. We use regex
rather than a full HTML parser internally because we want the
<template> inner content verbatim — a DOM-based parser would
require re-serializing children back to HTML and would normalize
quote / whitespace / attribute order along the way.
The parsed result is a Component value object. Order of templates
is preserved from the source; named templates remember their name.
Anonymous templates (no data-template attribute) are the
component's "default" markup; multiple anonymous templates are
allowed and concatenated in document order.
Defined Under Namespace
Classes: Component, ParseError, Template
Constant Summary collapse
- TEMPLATE_OPEN =
/<template(?:\s+data-template="([^"]*)")?\s*>/- TEMPLATE_CLOSE =
"</template>"- SCRIPT_OPEN =
/<script\s+type="text\/ruby"\s*>/- SCRIPT_CLOSE =
"</script>"
Class Method Summary collapse
-
.extract_inline_ruby_scripts(html, path: nil) ⇒ Object
Walk an arbitrary HTML string for
<script type="text/ruby">…</script>blocks (the same shape.lilfiles use). - .extract_script(source, match, path) ⇒ Object
- .extract_template(source, match, path) ⇒ Object
-
.parse(source, path: nil) ⇒ Object
Walks the source linearly so nested
<template>/<script>tags don't get matched by a greedy regex. - .parse_file(path) ⇒ Object
Class Method Details
.extract_inline_ruby_scripts(html, path: nil) ⇒ Object
Walk an arbitrary HTML string for <script type="text/ruby">…</script>
blocks (the same shape .lil files use). Returns the source strings in
document order plus an HTML copy with each block removed verbatim
(open tag + body + close tag). Other <script> types (e.g. module,
untyped) are untouched.
Used by the page-build path so a page may embed inline Ruby without being silently dropped on the compiled target.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/lilac/cli/build/sfc.rb', line 112 def self.extract_inline_ruby_scripts(html, path: nil) scripts = [] ranges = [] cursor = 0 while (script_match = html.match(SCRIPT_OPEN, cursor)) body_start = script_match.end(0) body_end = html.index(SCRIPT_CLOSE, body_start) or raise ParseError, "Unterminated <script type=\"text/ruby\"> in #{path || '<input>'}" scripts << html[body_start...body_end] ranges << (script_match.begin(0)...(body_end + SCRIPT_CLOSE.length)) cursor = body_end + SCRIPT_CLOSE.length end stripped = remove_ranges(html, ranges) { stripped_html: stripped, scripts: scripts } end |
.extract_script(source, match, path) ⇒ Object
96 97 98 99 100 101 102 |
# File 'lib/lilac/cli/build/sfc.rb', line 96 def self.extract_script(source, match, path) body_start = match.end(0) body_end = source.index(SCRIPT_CLOSE, body_start) raise ParseError, "Unterminated <script type=\"text/ruby\"> in #{path || '<input>'}" unless body_end source[body_start...body_end] end |
.extract_template(source, match, path) ⇒ Object
87 88 89 90 91 92 93 94 |
# File 'lib/lilac/cli/build/sfc.rb', line 87 def self.extract_template(source, match, path) name = match[1].to_s.empty? ? nil : match[1] body_start = match.end(0) body_end = source.index(TEMPLATE_CLOSE, body_start) raise ParseError, "Unterminated <template> in #{path || '<input>'}" unless body_end Template.new(name: name, body: source[body_start...body_end]) end |
.parse(source, path: nil) ⇒ Object
Walks the source linearly so nested <template> / <script> tags
don't get matched by a greedy regex. Each iteration:
1. find the earliest opening tag (template or script)
2. find its matching close, extract the body
3. advance past the close
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/lilac/cli/build/sfc.rb', line 57 def self.parse(source, path: nil) templates = [] script_parts = [] cursor = 0 while cursor < source.length tmpl_match = source.match(TEMPLATE_OPEN, cursor) script_match = source.match(SCRIPT_OPEN, cursor) # Earlier of the two openings wins; nil-safe sort by offset. next_match = [tmpl_match, script_match].compact .min_by { |m| m.begin(0) } break unless next_match if next_match == tmpl_match templates << extract_template(source, tmpl_match, path) cursor = source.index(TEMPLATE_CLOSE, tmpl_match.end(0)) + TEMPLATE_CLOSE.length else script_parts << extract_script(source, script_match, path) cursor = source.index(SCRIPT_CLOSE, script_match.end(0)) + SCRIPT_CLOSE.length end end Component.new( path: path, templates: templates, script: script_parts.join("\n"), ) end |
.parse_file(path) ⇒ Object
48 49 50 |
# File 'lib/lilac/cli/build/sfc.rb', line 48 def self.parse_file(path) parse(File.read(path), path: path) end |