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

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

Raises:



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

Raises:



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