Fix HAML extraction with embedded Ruby (#17846)

This PR fixes and improves the HAML extractor by ensuring that whenever
we detect lines or blocks of Ruby code (`-`, `=` and `~`) characters at
the beginning of the line, we treat them as Ruby code.

Fixes: #17813

## Test Plan

1. Existing tests pass
2. Changed 1 existing test which embedded Ruby syntax
3. Added a dedicated test to ensure the HAML file in the linked issue is
parsed correctly

Running this in the internal extractor tool you can see that the
`w-[12px]`, `w-[16px]`, `h-[12px]`, and `h-[16px]` are properly
extracted.

Note: the `mr-12px` is also extracted, but not highlighted because this
is not a valid Tailwind CSS class.

<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/fc5929ca-bc71-47d2-b21b-7abeec86f54d"
/>
This commit is contained in:
Robin Malfait 2025-05-05 16:26:17 +02:00 • committed by GitHub
parent 473f0241bf
commit d38554d110
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 682 additions and 159 deletions

View file

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Ensure negative arbitrary `scale` values generate negative values ([#17831](https://github.com/tailwindlabs/tailwindcss/pull/17831))
- Fix HAML extraction with embedded Ruby ([#17846](https://github.com/tailwindlabs/tailwindcss/pull/17846))
## [4.1.5] - 2025-04-30

7
Cargo.lock generated
View file

@ -582,6 +582,7 @@ dependencies = [
"tempfile",
"tracing",
"tracing-subscriber",
"unicode-width",
"walkdir",
]
@ -681,6 +682,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "valuable"
version = "0.1.0"

View file

@ -23,3 +23,4 @@ regex = "1.11.1"
[dev-dependencies]
tempfile = "3.13.0"
pretty_assertions = "1.4.1"
unicode-width = "0.2.0"

View file

@ -3,6 +3,8 @@ use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::machine::{Machine, MachineState};
use crate::extractor::pre_processors::pre_processor::PreProcessor;
use crate::extractor::variant_machine::VariantMachine;
use crate::scanner::pre_process_input;
use bstr::ByteVec;
#[derive(Debug, Default)]
pub struct Haml;
@ -14,8 +16,153 @@ impl PreProcessor for Haml {
let mut cursor = cursor::Cursor::new(content);
let mut bracket_stack = BracketStack::default();
// Haml Comments: -#
// https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-evaluation
//
// > The hyphen followed immediately by the pound sign signifies a silent comment. Any text
// > following this isn’t rendered in the resulting document at all.
//
// ```haml
// %p foo
// -# This is a comment
// %p bar
// ```
//
// > You can also nest text beneath a silent comment. None of this text will be rendered.
//
// ```haml
// %p foo
// -#
// This won't be displayed
// Nor will this
// Nor will this.
// %p bar
// ```
//
// Ruby Evaluation
// https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-evaluation
//
// When any of the following characters are the first non-whitespace character on the line,
// then the line is treated as Ruby code:
//
// - Inserting Ruby: =
// https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby
//
// ```haml
// %p
// = ['hi', 'there', 'reader!'].join " "
// = "yo"
// ```
//
// - Running Ruby: -
// https://haml.info/docs/yardoc/file.REFERENCE.html#running-ruby--
//
// ```haml
// - foo = "hello"
// - foo << " there"
// - foo << " you!"
// %p= foo
// ```
//
// - Whitespace Preservation: ~
// https://haml.info/docs/yardoc/file.REFERENCE.html#tilde
//
// > ~ works just like =, except that it runs Haml::Helpers.preserve on its input.
//
// ```haml
// ~ "Foo\n<pre>Bar\nBaz</pre>"
// ```
//
// Important note:
//
// > A line of Ruby code can be stretched over multiple lines as long as each line but the
// > last ends with a comma.
//
// ```haml
// - links = {:home => "/",
// :docs => "/docs",
// :about => "/about"}
// ```
//
// Ruby Blocks:
// https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-blocks
//
// > Ruby blocks, like XHTML tags, don’t need to be explicitly closed in Haml. Rather,
// > they’re automatically closed, based on indentation. A block begins whenever the
// > indentation is increased after a Ruby evaluation command. It ends when the indentation
// > decreases (as long as it’s not an else clause or something similar).
//
// ```haml
// - (42...47).each do |i|
// %p= i
// %p See, I can count!
// ```
//
let mut last_newline_position = 0;
while cursor.pos < len {
match cursor.curr {
// Escape the next character
b'\\' => {
cursor.advance_twice();
continue;
}
// Track the last newline position
b'\n' => {
last_newline_position = cursor.pos;
cursor.advance();
continue;
}
// Skip HAML comments. `-#`
b'-' if cursor.input[last_newline_position..cursor.pos]
.iter()
.all(u8::is_ascii_whitespace)
&& matches!(cursor.next, b'#') =>
{
// Just consume the comment
let updated_last_newline_position =
self.skip_indented_block(&mut cursor, last_newline_position);
// Override the last known newline position
last_newline_position = updated_last_newline_position;
}
// Skip HTML comments. `/`
b'/' if cursor.input[last_newline_position..cursor.pos]
.iter()
.all(u8::is_ascii_whitespace) =>
{
// Just consume the comment
let updated_last_newline_position =
self.skip_indented_block(&mut cursor, last_newline_position);
// Override the last known newline position
last_newline_position = updated_last_newline_position;
}
// Ruby evaluation
b'-' | b'=' | b'~'
if cursor.input[last_newline_position..cursor.pos]
.iter()
.all(u8::is_ascii_whitespace) =>
{
let mut start = cursor.pos;
let end = self.skip_indented_block(&mut cursor, last_newline_position);
// Increment start with 1 character to skip the `=` or `-` character
start += 1;
let ruby_code = &cursor.input[start..end];
// Override the last known newline position
last_newline_position = end;
let replaced = pre_process_input(ruby_code, "rb");
result.replace_range(start..end, replaced);
}
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
//
// ```diff
@ -89,6 +236,107 @@ impl PreProcessor for Haml {
}
}
impl Haml {
fn skip_indented_block(
&self,
cursor: &mut cursor::Cursor,
last_known_newline_position: usize,
) -> usize {
let len = cursor.input.len();
// Special case: if the first character of the block is `=`, then newlines are only allowed
// _if_ the last character of the previous line is a comma `,`.
//
// https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby
//
// > A line of Ruby code can be stretched over multiple lines as long as each line but the
// > last ends with a comma. For example:
//
// ```haml
// = link_to_remote "Add to cart",
// :url => { :action => "add", :id => product.id },
// :update => { :success => "cart", :failure => "error" }
// ```
let evaluation_type = cursor.curr;
let block_indentation_level = cursor
.pos
.saturating_sub(last_known_newline_position)
.saturating_sub(1); /* The newline itself */
let mut last_newline_position = last_known_newline_position;
// Consume until the end of the line first
while cursor.pos < len && cursor.curr != b'\n' {
cursor.advance();
}
// Block is already done, aka just a line
if evaluation_type == b'=' && cursor.prev != b',' {
return cursor.pos;
}
'outer: while cursor.pos < len {
match cursor.curr {
// Escape the next character
b'\\' => {
cursor.advance_twice();
continue;
}
// Track the last newline position
b'\n' => {
last_newline_position = cursor.pos;
// We are done with this block
if evaluation_type == b'=' && cursor.prev != b',' {
break;
}
cursor.advance();
continue;
}
// Skip whitespace and compute the indentation level
x if x.is_ascii_whitespace() => {
// Find first non-whitespace character
while cursor.pos < len && cursor.curr.is_ascii_whitespace() {
if cursor.curr == b'\n' {
last_newline_position = cursor.pos;
if evaluation_type == b'=' && cursor.prev != b',' {
// We are done with this block
break 'outer;
}
}
cursor.advance();
}
let indentation = cursor
.pos
.saturating_sub(last_newline_position)
.saturating_sub(1); /* The newline itself */
if indentation < block_indentation_level {
// We are done with this block
break;
}
}
// Not whitespace, end of block
_ => break,
};
cursor.advance();
}
// Move the cursor to the last newline position
cursor.move_to(last_newline_position);
last_newline_position
}
}
#[cfg(test)]
mod tests {
use super::Haml;
@ -173,10 +421,18 @@ mod tests {
// https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
#[test]
fn test_haml_full_file() {
let processed = Haml.process(include_bytes!("./test-fixtures/haml/src-1.haml"));
let actual = std::str::from_utf8(&processed).unwrap();
let expected = include_str!("./test-fixtures/haml/dst-1.haml");
fn test_haml_full_file_17051() {
let actual = Haml::extract_annotated(include_bytes!("./test-fixtures/haml/src-17051.haml"));
let expected = include_str!("./test-fixtures/haml/dst-17051.haml");
assert_eq!(actual, expected);
}
// https://github.com/tailwindlabs/tailwindcss/issues/17813
#[test]
fn test_haml_full_file_17813() {
let actual = Haml::extract_annotated(include_bytes!("./test-fixtures/haml/src-17813.haml"));
let expected = include_str!("./test-fixtures/haml/dst-17813.haml");
assert_eq!(actual, expected);
}

View file

@ -57,4 +57,104 @@ pub trait PreProcessor: Sized + Default {
panic!("Missing some items");
}
}
#[cfg(test)]
fn extract_annotated(input: &[u8]) -> String {
use crate::extractor::{Extracted, Extractor};
use std::collections::BTreeMap;
use unicode_width::UnicodeWidthStr;
let processor = Self::default();
let transformed = processor.process(input);
let extracted = Extractor::new(&transformed).extract();
// Extract only candidate positions
let byte_ranges = extracted
.iter()
.filter_map(|x| match x {
Extracted::Candidate(bytes) => {
let start = bytes.as_ptr() as usize - transformed.as_ptr() as usize;
let end = start + bytes.len();
Some((start, end))
}
_ => None,
})
.collect::<Vec<_>>();
// Convert byte ranges to (line, start_col, end_col)
let mut annotations = byte_ranges
.into_iter()
.map(|(start, end)| {
let (line, start_col) = byte_offset_to_line_and_column(input, start);
let (_, end_col) = byte_offset_to_line_and_column(input, end);
(line, start_col, end_col)
})
.collect::<Vec<_>>();
// Sort for safe insertion
annotations.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
// Convert input to lines
let mut lines = std::str::from_utf8(input)
.expect("Input must be valid UTF-8")
.lines()
.map(|line| line.to_string())
.collect::<Vec<_>>();
// Group annotations per line
let mut grouped = BTreeMap::<usize, Vec<(usize, usize)>>::new();
for (line, start_char, end_char) in annotations {
grouped
.entry(line)
.or_default()
.push((start_char, end_char));
}
// Inject annotation lines
for (line_idx, spans) in grouped.into_iter().rev() {
let display_line = &lines[line_idx];
let width = UnicodeWidthStr::width(display_line.as_str());
let mut annotation = vec![' '; width];
for (start, end) in spans {
for i in start..end.min(annotation.len()) {
annotation[i] = '^';
}
}
let annotation_line: String = annotation
.into_iter()
.collect::<String>()
.trim_end()
.to_owned();
lines.insert(line_idx + 1, annotation_line);
}
lines.join("\n").trim_end().to_string() + "\n"
}
}
#[cfg(test)]
fn byte_offset_to_line_and_column(input: &[u8], offset: usize) -> (usize, usize) {
use unicode_width::UnicodeWidthStr;
let mut line_start = 0;
let mut line = 0;
for (i, &b) in input.iter().enumerate() {
if i >= offset {
break;
}
if b == b'\n' {
line += 1;
line_start = i + 1;
}
}
let slice = &input[line_start..offset];
let column = std::str::from_utf8(slice).expect("Valid UTF-8");
let column = UnicodeWidthStr::width(column);
(line, column)
}

View file

@ -1,155 +0,0 @@
/ https://github com/tailwindlabs/tailwindcss/pull/17051 issuecomment-2711181352
- star_styles "size- 800px mask mask-star bg-gradient-to-r from-secondary via-cyan-400 to-lime-400"
- crazy_text_styles "italic font-black bg-gradient-to-r via-60% to-90% from-orange-500 via-secondary to-primary text-transparent bg-clip-text inline-block py-2"
relative
- Blurred background star
absolute left-0 z-0 class: "-top-[400px] -right-[400px]"
flex justify-end blur-3xl
%div class: star_styles
relative z-10
%h1 mt-8 text-center text-5xl font-black tracking-wide drop-shadow-lg
%div
Components, Guides, and Paradigms
md:inline-flex md:items-center justify-center
%span -mr-2
for
%span relative -rotate-3 hover:-rotate-6 hover:scale-125 transition-transform
%span class: crazy_text_styles + " absolute blur-xl"
&nbsp;CRAZY&nbsp;
%span class: crazy_text_styles + " drop-shadow-[0_0_1px_#fff]"
&nbsp;CRAZY&nbsp;
%span -ml-3 md:-ml-1
\-fast Development
%div
in Ruby on Rails!
mt-16 text-center
daisy_button "😃 Get Started", css: "btn-primary text-xl",
right_icon: "arrow-right", target: "_blank",
href: "https://github com/profoundry-us/loco_motion locomotion-components"
mt-32 lg:flex lg:space-x-8
%div class: "md:basis-2/5"
%h3 text-4xl font-bold
Easy, Flexible Components
%p mt-2 text-xl
Powered by the fabulous
succeed ',' do
daisy_link "ViewComponent", "https://viewcomponent.org", target: "_blank"
succeed ', and' do
daisy_link "DaisyUI", "https://daisyui com/", target: "_blank"
daisy_link "TailwindCSS", "https://tailwindcss com/", target: "_blank"
libraries, our components are designed to be fast, flexible, and easy to
use <i>directy</i> in Ruby on Rails!
doc_example css: "mt-8 lg:mt-0 lg:basis-3/5 h-44" do
flex flex-col sm:flex-row items-center gap-4
daisy_button "Accent Button", css: "btn-accent"
daisy_tip "Click to Swap" do
daisy_swap off: "🌚", on: "🌞", css: "swap-rotate text-4xl"
daisy_badge "Large Badge", css: "badge-secondary badge-lg"
mt-32 xl:flex xl:flex-row-reverse xl:items-center
text-xl xl:ml-8
%h3 text-4xl font-bold
Simple, Concise Views
%p mt-2
Utlize
daisy_link "HAML", "https://haml.info/", target: "_blank"
so your views are simple, concise, and easy to understand
%p mt-2
No more messy ERB files with all of their closing tags and Ruby wrappers
HAML feels more natural to write and reduces file sizes, making your
views easier to read and maintain
%p mt-2
:markdown
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
Textile, and many more!
mt-8 xl:mt-0
flex flex-col xl:flex xl:flex-row w-full
doc_code css: "grow", code_css: "xl:pl-2 xl:pr-0 xl:rounded-r-none", language: "erb" do
:escaped
<% Ruby %>
<% 5 times do |i| %>
<% if i even? %>
<p class "odd">Number <% i %></p>
<% else %>
<p class "even">Numero <% i %></p>
<% end %>
<% end %>
doc_code css: "grow mt-8 xl:mt-0", pre_css: "xl:h-full", code_css: "xl:pl-0 xl:pr-2 xl:h-full xl:rounded-l-none" do
:escaped
- HAML
- 5 times do |i|
- if i even?
%p odd Number \ i
- else
%p even Numero \ i
%h3 mt-32 text-4xl font-bold
Build Your <i>OWN</i> Components
%p mt-2 text-xl
Can't find exactly what you need? No problem! Build your own components
with ease using our simple, flexible, and powerful DSL
mt-8 xl:flex xl:space-x-8
%div
doc_code language: "ruby" do
:escaped
app/components/application_component rb
class ApplicationComponent < LocoMotion::BaseComponent
Add your custom / shared component logic here!
end
doc_code language: "haml", css: "mt-8" do
:escaped
- app/components/character_component html haml
part :component do
part :head
part :body do
content
part :legs
%div mt-8 xl:mt-0
doc_code language: "ruby" do
:escaped
app/components/character_component rb
class CharacterComponent < ApplicationComponent
define_parts :head, :body, :legs
def before_render
set_tag_name :head, :h1
add_css :head, "text-3xl font-bold"
set_tag_name :body, :p
add_stimulus_controller :body, "character-body"
add_css :body, "text-lg"
set_tag_name :legs, :footer
add_css :legs, "text-sm"
end
end
%h3 mt-24 text-center text-4xl font-bold italic
More Coming Soon!
%p mt-2 text-xl text-center
Keen an eye out as we'll be adding more components, guides, and
%br max-sm:hidden
suggested gems for you to build amazing Rails apps!
mt-4 text-center
daisy_button "😉 Get Started", css: "btn-primary text-xl",
right_icon: "arrow-right", target: "_blank",
class: "px-2.5"
href: "https://github com/profoundry-us/loco_motion locomotion-components"

View file

@ -0,0 +1,264 @@
/ https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
- star_styles = "size-[800px] mask mask-star bg-gradient-to-r from-secondary via-cyan-400 to-lime-400"
^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^
- crazy_text_styles = "italic font-black bg-gradient-to-r via-60% to-90% from-orange-500 via-secondary to-primary text-transparent bg-clip-text inline-block py-2"
^^^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^
.relative
^^^^^^^^
- # Blurred background star
^^^^^^^^^^ ^^^^
.absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" }
^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^
.flex.justify-end.blur-3xl
^^^^ ^^^^^^^^^^^ ^^^^^^^^
%div{ class: star_styles }
^^^^^ ^^^^^^^^^^^
.relative.z-10
^^^^^^^^ ^^^^
%h1.mt-8.text-center.text-5xl.font-black.tracking-wide.drop-shadow-lg
^^^^ ^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
%div
Components, Guides, and Paradigms
^^^
.md:inline-flex.md:items-center.justify-center
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
%span.-mr-2
^^^^^
for
^^^
%span.relative.-rotate-3.hover:-rotate-6.hover:scale-125.transition-transform
^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
%span{ class: crazy_text_styles + " absolute blur-xl" }
^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^
&nbsp;CRAZY&nbsp;
%span{ class: crazy_text_styles + " drop-shadow-[0_0_1px_#fff]" }
^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
&nbsp;CRAZY&nbsp;
%span.-ml-3.md:-ml-1
^^^^^ ^^^^^^^^
\-fast Development
%div
in Ruby on Rails!
^^ ^^
.mt-16.text-center
^^^^^ ^^^^^^^^^^^
= daisy_button "😃 Get Started", css: "btn-primary text-xl",
^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^ ^^^^^^^
right_icon: "arrow-right", target: "_blank",
^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^
href: "https://github.com/profoundry-us/loco_motion#locomotion-components"
^^^^ ^^^^^^^^^^^^^^^^^^^^^
.mt-32.lg:flex.lg:space-x-8
^^^^^ ^^^^^^^ ^^^^^^^^^^^^
%div{ class: "md:basis-2/5" }
^^^^^ ^^^^^^^^^^^^
%h3.text-4xl.font-bold
^^^^^^^^ ^^^^^^^^^
Easy, Flexible Components
%p.mt-2.text-xl
^^^^ ^^^^^^^
Powered by the fabulous
^^ ^^^ ^^^^^^^^
= succeed ',' do
^^^^^^^ ^^
= daisy_link("ViewComponent", "https://viewcomponent.org", target: "_blank")
^^^ ^^^^^^
= succeed ', and' do
^^^^^^^ ^^^ ^^
= daisy_link "DaisyUI", "https://daisyui.com/", target: "_blank"
^^^^^^^^^^ ^^^^^^
= daisy_link "TailwindCSS", "https://tailwindcss.com/", target: "_blank"
^^^^^^^^^^ ^^^^^^
libraries, our components are designed to be fast, flexible, and easy to
^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^ ^^ ^^ ^^^ ^^^^ ^^
use <i>directy</i> in Ruby on Rails!
^^^ ^^^^^^^ ^^ ^^
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
^^^ ^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^
.flex.flex-col.sm:flex-row.items-center.gap-4
^^^^ ^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^
= daisy_button "Accent Button", css: "btn-accent"
^^^^^^^^^^^^ ^^^ ^^^^^^^^^^
= daisy_tip("Click to Swap") do
^^ ^^
= daisy_swap off: "🌚", on: "🌞", css: "swap-rotate text-4xl"
^^^^^^^^^^ ^^^ ^^ ^^^ ^^^^^^^^^^^ ^^^^^^^^
= daisy_badge "Large Badge", css: "badge-secondary badge-lg"
^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^
.mt-32.xl:flex.xl:flex-row-reverse.xl:items-center
^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
.text-xl.xl:ml-8
^^^^^^^ ^^^^^^^
%h3.text-4xl.font-bold
^^^^^^^^ ^^^^^^^^^
Simple, Concise Views
%p.mt-2
^^^^
Utlize
= daisy_link("HAML", "https://haml.info/", target: "_blank")
^^^^^^
so your views are simple, concise, and easy to understand.
^^ ^^^^ ^^^^^ ^^^ ^^^ ^^^^ ^^ ^^^^^^^^^^
%p.mt-2
^^^^
No more messy ERB files with all of their closing tags and Ruby wrappers.
^^^^ ^^^^^ ^^^^^ ^^^^ ^^^ ^^ ^^^^^ ^^^^^^^ ^^^^ ^^^ ^^^^^^^^
HAML feels more natural to write and reduces file sizes, making your
^^^^^ ^^^^ ^^^^^^^ ^^ ^^^^^ ^^^ ^^^^^^^ ^^^^ ^^^^^^ ^^^^
views easier to read and maintain.
^^^^^ ^^^^^^ ^^ ^^^^ ^^^ ^^^^^^^^
%p.mt-2
^^^^
:markdown
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
^^^ ^^^^^^ ^^^^^^^ ^^^^
Textile, and many more!
^^^ ^^^^ ^^^^^
.mt-8.xl:mt-0
^^^^ ^^^^^^^
.flex.flex-col.xl:flex.xl:flex-row.w-full
^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^
= doc_code(css: "grow", code_css: "xl:pl-2 xl:pr-0 xl:rounded-r-none", language: "erb") do
^^^ ^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^ ^^
:escaped
<% # Ruby %>
<% 5.times do |i| %>
^^^^^ ^^
<% if i.even? %>
^^ ^
<p class="odd">Number <%= i %></p>
^^^^^ ^^^ ^
<% else %>
^^^^
<p class="even">Numero <%= i %></p>
^^^^^ ^^^^ ^
<% end %>
^^^
<% end %>
^^^
= doc_code(css: "grow mt-8 xl:mt-0", pre_css: "xl:h-full", code_css: "xl:pl-0 xl:pr-2 xl:h-full xl:rounded-l-none") do
^^^ ^^^^ ^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^
:escaped
- # HAML
- 5.times do |i|
^^^^^ ^^
- if i.even?
^^
%p.odd Number \#{i}
^^^ ^
- else
^^^^
%p.even Numero \#{i}
^^^^ ^
%h3.mt-32.text-4xl.font-bold
^^^^^ ^^^^^^^^ ^^^^^^^^^
Build Your <i>OWN</i> Components
%p.mt-2.text-xl
^^^^ ^^^^^^^
Can't find exactly what you need? No problem! Build your own components
^ ^^^^ ^^^^^^^ ^^^^ ^^^ ^^^^^^^^ ^^^^ ^^^ ^^^^^^^^^^
with ease using our simple, flexible, and powerful DSL.
^^^^ ^^^^ ^^^^^ ^^^ ^^^ ^^^^^^^^
.mt-8.xl:flex.xl:space-x-8
^^^^ ^^^^^^^ ^^^^^^^^^^^^
%div
= doc_code(language: "ruby") do
^^^^^^^^ ^^^^ ^^
:escaped
# app/components/application_component.rb
^^
class ApplicationComponent < LocoMotion::BaseComponent
^^^^^
# Add your custom / shared component logic here!
^^^^ ^^^^^^ ^^^^^^ ^^^^^^^^^ ^^^^^ ^^^^^
end
^^^
= doc_code(language: "haml", css: "mt-8") do
^^^^^^^^ ^^^^ ^^^ ^^^^ ^^
:escaped
- # app/components/character_component.html.haml
^^^^
= part(:component) do
^^
= part(:head)
= part(:body) do
^^
= content
^^^^^^^
= part(:legs)
%div.mt-8.xl:mt-0
^^^^ ^^^^^^^
= doc_code(language: "ruby") do
^^^^^^^^ ^^^^ ^^
:escaped
# app/components/character_component.rb
^^
class CharacterComponent < ApplicationComponent
^^^^^
define_parts :head, :body, :legs
^^^^^^^^^^^^
def before_render
^^^ ^^^^^^^^^^^^^
set_tag_name(:head, :h1)
^^^^^^^^^^^^
add_css(:head, "text-3xl font-bold")
^^^^^^^ ^^^^^^^^ ^^^^^^^^^
set_tag_name(:body, :p)
^^^^^^^^^^^^
add_stimulus_controller(:body, "character-body")
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
add_css(:body, "text-lg")
^^^^^^^ ^^^^^^^
set_tag_name(:legs, :footer)
^^^^^^^^^^^^
add_css(:legs, "text-sm")
^^^^^^^ ^^^^^^^
end
^^^
end
^^^
%h3.mt-24.text-center.text-4xl.font-bold.italic
^^^^^ ^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^ ^^^^^^
More Coming Soon!
%p.mt-2.text-xl.text-center
^^^^ ^^^^^^^ ^^^^^^^^^^^
Keen an eye out as we'll be adding more components, guides, and
^^ ^^^ ^^^ ^^ ^^ ^^ ^^ ^^^^^^ ^^^^ ^^^
%br.max-sm:hidden
^^^^^^^^^^^^^
suggested gems for you to build amazing Rails apps!
^^^^^^^^^ ^^^^ ^^^ ^^^ ^^ ^^^^^ ^^^^^^^ ^^^^^
.mt-4.text-center
^^^^ ^^^^^^^^^^^
= daisy_button "😉 Get Started", css: "btn-primary text-xl",
^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^ ^^^^^^^
right_icon: "arrow-right", target: "_blank",
^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^
class: "px-2.5"
^^^^^ ^^^^^^
href: "https://github.com/profoundry-us/loco_motion#locomotion-components"
^^^^ ^^^^^^^^^^^^^^^^^^^^^

View file

@ -0,0 +1,30 @@
:ruby
- devices_classes = 'size-5 mr-2px'
^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^
- icon_classes = 'w-[12px] h-[12px]'
^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^
!!!
%html{ lang: 'en' }
^^^^ ^^
%head
%title Tailwind v4.1.4 + HAML bug
^^^^^^ ^^^
-# This is a comment
^^ ^ ^^^^^^^
A multi-line comment
^^^^^^^^^^ ^^^^^^^
With more indentation
^^^^ ^^^^^^^^^^^
Which can dedent again just fine
^^^ ^^^^^^ ^^^^^ ^^^^ ^^^^
%body
- icon_classes = 'self-center w-[16px] h-[16px]'
^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^
.flex{ class: icon_classes }
^^^^ ^^^^^ ^^^^^^^^^^^^
.flex{ class: devices_classes }
^^^^ ^^^^^ ^^^^^^^^^^^^^^^

View file

@ -0,0 +1,19 @@
:ruby
- devices_classes = 'size-5 mr-2px'
- icon_classes = 'w-[12px] h-[12px]'
!!!
%html{ lang: 'en' }
%head
%title Tailwind v4.1.4 + HAML bug
-# This is a comment
A multi-line comment
With more indentation
Which can dedent again just fine
%body
- icon_classes = 'self-center w-[16px] h-[16px]'
.flex{ class: icon_classes }
.flex{ class: devices_classes }