Skip to content

Instantly share code, notes, and snippets.

@easylogic
Last active December 19, 2023 03:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save easylogic/32aea71e5f2c583b13cc1b1c663e6bfd to your computer and use it in GitHub Desktop.
Save easylogic/32aea71e5f2c583b13cc1b1c663e6bfd to your computer and use it in GitHub Desktop.
stylish language - CSS and Language - SaLang - 사랑( korean language)

사랑(SaLang - Stylish CSS based Language)

salang is css based language.

please show below code

span.MyComponent {
    // properties 
    --background-color: yellow;
    --label-color: black;
   
    // css 
    background-color: var(--background-color);
    
    label {
        span {
            color: var(--label-color);
            
            "안녕하세요";
        }
    }
    
    @click () {
      --background-color: blue;
      --label-color: yellow;
    }
}

Above code is converted to some files.

span.MyComponent {

    // properties 
    --background-color: yellow;
   
    // css 
    background-color: var(--background-color);
    
    label {
        span {
            color: var(--label-color);
        }
    }

}
class MyComponent extends UIElement {
    template() {
        return <span class="MyComponent">
          <label>
            <span>
              안녕하세요.
            </span>
          </label>
        </span>
    }
    
    [CLICK()] () {
      this.$el.cssText(`
        --background-color: blue;
        --label-color: yellow;
      `)
    }

}
@easylogic
Copy link
Author

컨셉

  1. 웹 컴포넌트 만들기 시스템
  2. CSS 변수의 JS 호환
  3. 내부 템플릿 자동 정의
  4. @component, @function 지원
  5. @function js 코드로 구현
  6. @function 안에서 css 문법 사용 가능

@easylogic
Copy link
Author

easylogic commented Dec 16, 2023

/* 커스텀 컴포넌트 my-button 정의 */
@component my-button {
    width: 100px;
    height: 50px;
    background-color: lightblue;

    :hover: {
        background-color: lightgreen;
    }

    /* JavaScript 스타일 이벤트 핸들러 정의 */
    @event click(() => {
        event.target {
            background-color: red;
        }
    });
}

/* 커스텀 컴포넌트 my-button 내부에 다른 컴포넌트 추가 예시 */
my-button {
    text: "Click Me";
    
    // 외부에서 children 을 주입하는 형태 
    @body;

    /* 컴포넌트 내부의 또 다른 컴포넌트 */
    @component nested-component {
        --content-text: string;
        --font-size: string = "16px";

        display: block;
        background-color: lightcoral;
        padding: 5px;

        body {
            div.inner-content {
                font-size: var(--font-size);
                color: navy;
                text: var(--content-text);

                @body;
            }
        }
    }
}

@easylogic
Copy link
Author

easylogic commented Dec 16, 2023

my-button {
    nested-component {.  // my-button 내부에서 정의된 nested-component 를 표현함 
         --content-text: blue;
         p { 
              background-color: yellow;
         }
    }
}

@easylogic
Copy link
Author

변수 선언은 기존 css 그대로 하기
js 구문에서도 var(--xxx) 지원하기

@easylogic
Copy link
Author

easylogic commented Dec 16, 2023

@Body 를 구성하는 방법 연구하기

body {
    "xxxx"
    p { 
        color: blue; 
        body {
             "yellow"
        }
    }
}

@easylogic
Copy link
Author

새로운 CSS 기반 언어 백서 (업데이트된 버전)

개요

이 백서는 새로운 CSS 기반 언어의 구조와 문법을 설명합니다. 이 언어는 웹 컴포넌트의 스타일, 구조, 동작을 선언적으로 정의하고 관리하며, 웹 개발의 효율성과 유지보수성을 향상시키는 것을 목표로 합니다.

주요 개념

컴포넌트 정의 (@component): 웹 컴포넌트의 스타일, 구조 및 동작을 정의합니다.

  • 변수 (@var): 컴포넌트의 스타일 및 동작에 사용되는 변수를 선언합니다.
  • 템플릿 (@template): 컴포넌트의 HTML 구조를 선언적으로 정의합니다.
  • 속성 (@Attribute): 컴포넌트의 속성을 정의하고, 동적으로 값을 할당합니다.
  • 반응형 디자인 (@media): 미디어 쿼리를 사용하여 반응형 디자인을 구현합니다.
  • 이벤트 핸들링 (@event): 사용자 상호작용에 대한 이벤트 핸들러를 정의합니다.
  • JavaScript 통합 (@js): 컴포넌트 내에서 JavaScript 코드를 실행합니다.

@easylogic
Copy link
Author

@var 는 기존의 CSS 문법으로도 정의 할 수 있다.

@var { 
    text: "xxxx";
}

or 

--text: "xxxx";

@easylogic
Copy link
Author

@component header-component {
  @var {
    backgroundColor: #007bff; // 색상 직접 지정
    textColor: white;
    title: "Welcome to Our Website";
  }

  @body {
    background-color: var(--backgroundColor);
    color: var(--textColor);
    padding: 15px;
    text-align: center;

    @template {
      h1 { var(--title) }
    }
  }
}

@component main-component {
  @var {
    backgroundColor: #e9ecef;
    mainText: "Main content goes here.";
  }

  @body {
    background-color: var(--backgroundColor);
    padding: 15px;

    @template {
      p { var(--mainText) }
    }
  }
}

@component footer-component {
  @var {
    backgroundColor: #343a40;
    textColor: white;
    footerText: "Footer content here";
  }

  @body {
    background-color: var(--backgroundColor);
    color: var(--textColor);
    padding: 15px;
    text-align: center;

    @template {
      p { var(--footerText) }
    }
  }
}

@component custom-button {
  @var {
    buttonColor: #007bff;
    textColor: white;
    buttonText: "Click Me";
    clickedColor: red;
  }

  @body {
    background-color: var(--buttonColor);
    color: var(--textColor);
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    cursor: pointer;

    @template {
      button {
        content: var(--buttonText);
      }
    }

    @event click @js {
      this {
        background-color: var(--clickedColor);
      }
      console.log('Button clicked!');
    }
  }
}

@component web-page {
  @use header-component;
  @use main-component;
  @use footer-component;
  @use custom-button;

  @var {
    mobileBreakpoint: 768px;
  }

  @body {
    display: grid;
    grid-template-areas: 
      "header"
      "main"
      "button"
      "footer";
    grid-template-rows: auto 1fr auto auto;
    height: 100vh;

    @template {
      header-component { grid-area: header; }
      main-component { grid-area: main; }
      custom-button { grid-area: button; }
      footer-component { grid-area: footer; }
    }

    @media (max-width: var(--mobileBreakpoint)) {
      header-component { font-size: 14px; }
      main-component { font-size: 14px; }
      footer-component { font-size: 14px; }
      custom-button { font-size: 14px; }
    }
  }
}

@easylogic
Copy link
Author

PEG.js 코드

// PEG.js 코드
{
  const astBuilder = require('./ast-builder'); // AST 빌더 코드

  // 문법 규칙 정의
  start
    = definition*

  definition
    = "/*" ~ (!"*/" .)* "*/"
    / component_definition

  component_definition
    = "@component" space name:component_name space "{" space (attribute / state / event / keyframe / conditional_rendering / template / text_content)+ space "}"

  component_name
    = identifier

  attribute
    = "@attribute" space "{" space name:identifier space ":" space value:value space "}"

  state
    = "@state" space "{" space name:css_variable space ":" space value:literal space "}"

  event
    = "@event" space name:identifier space "{" space "this" space "{" space code:code space "}" space "}"

  keyframe
    = "@keyframes" space name:identifier space "{" space keyframes:(
        space keyframe_step:(
          space rule:property_declaration
        )+ space
      )? "}"

  conditional_rendering
    = "@if" space "(" space condition:expression space ")" space "{" space body:(space property_declaration)* space "}" space "@else" space "{" space else_body:(space property_declaration)* space "}"

  template
    = "@template" space "{" space element:html_element space "}"

  html_element
    = identifier space "{" space properties:(
        space property_set:(
          space property_declaration
        )+ space
      )? space "}"

  text_content
    = identifier space "{" space "}"


  // 공백 문자 정의
  space
    = [ \t\n\r]*

  // 식별자 정의
  identifier
    = [a-zA-Z_][a-zA-Z0-9_-]*

  // 리터럴 값 정의
  literal
    = '"' text:([^"]*) '"' { return text; }
    / "'" text:([^']*) "'" { return text; }
    / number

  // 숫자 정의
  number
    = [0-9]+ ("." [0-9]*)? { return parseFloat(text()); }

  // 속성 선언 정의
  property_declaration
    = name:css_variable space ":" space value:property_value space ";"

  // CSS 변수 정의
  css_variable
    = "--" identifier { return text(); }

  // 속성 값 정의
  property_value
    = literal
    / css_variable

  // 식별자 또는 문자열 값 정의
  expression
    = identifier
    / literal

  // 코드 블록 정의
  code
    = "{" code: (!("}" / "{") .)* "}" { return code.join(""); }

  // 파서 시작점
  {
    return start();
  }
}

@easylogic
Copy link
Author

SaLang AST

{
  "type": "Program",
  "body": [
    {
      "type": "FunctionDefinition",
      "name": "toggle",
      "parameters": ["param"],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ReturnStatement",
            "value": {
              "type": "UnaryExpression",
              "operator": "!",
              "argument": {
                "type": "Identifier",
                "name": "param"
              }
            }
          }
        ]
      }
    },
    {
      "type": "ComponentDefinition",
      "name": "my-button",
      "attributes": [
        {
          "name": "label",
          "value": {
            "type": "StringLiteral",
            "value": "Click me"
          }
        }
      ],
      "states": [
        {
          "name": "--isActive",
          "initialValue": {
            "type": "BooleanLiteral",
            "value": false
          }
        }
      ],
      "events": [
        {
          "name": "click",
          "handler": {
            "type": "EventHandler",
            "code": {
              "type": "BlockStatement",
              "body": [
                {
                  "type": "AssignmentExpression",
                  "left": {
                    "type": "CSSVariable",
                    "name": "--isActive"
                  },
                  "operator": "=",
                  "right": {
                    "type": "CallExpression",
                    "callee": {
                      "type": "Identifier",
                      "name": "toggle"
                    },
                    "arguments": [
                      {
                        "type": "CSSVariable",
                        "name": "--isActive"
                      }
                    ]
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "background-color",
                  "value": {
                    "type": "ConditionalExpression",
                    "test": {
                      "type": "CSSVariable",
                      "name": "--isActive"
                    },
                    "consequent": {
                      "type": "CSSVariable",
                      "name": "--primaryColor"
                    },
                    "alternate": {
                      "type": "CSSVariable",
                      "name": "--secondaryColor"
                    }
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "animation",
                  "value": {
                    "type": "CallExpression",
                    "callee": {
                      "type": "Identifier",
                      "name": "fade-in"
                    },
                    "arguments": [
                      {
                        "type": "CSSVariable",
                        "name": "--animationDuration"
                      }
                    ]
                  }
                }
              ]
            }
          }
        }
      ],
      "keyframes": [
        {
          "name": "fade-in",
          "keyframes": [
            {
              "type": "KeyframeStep",
              "rule": {
                "type": "PropertyDeclaration",
                "name": "opacity",
                "value": {
                  "type": "NumericLiteral",
                  "value": 0
                }
              }
            },
            {
              "type": "KeyframeStep",
              "rule": {
                "type": "PropertyDeclaration",
                "name": "opacity",
                "value": {
                  "type": "NumericLiteral",
                  "value": 1
                }
              }
            }
          ]
        }
      ],
      "conditionalRendering": [
        {
          "condition": {
            "type": "CSSVariable",
            "name": "--isActive"
          },
          "body": [
            {
              "type": "PropertyDeclaration",
              "name": "display",
              "value": {
                "type": "Keyword",
                "value": "block"
              }
            }
          ]
        },
        {
          "condition": {
            "type": "UnaryExpression",
            "operator": "!",
            "argument": {
              "type": "CSSVariable",
              "name": "--isActive"
            }
          },
          "body": [
            {
              "type": "PropertyDeclaration",
              "name": "display",
              "value": {
                "type": "Keyword",
                "value": "none"
              }
            }
          ]
        }
      ],
      "template": {
        "type": "HTMLElement",
        "name": "button",
        "attributes": [
          {
            "name": "style",
            "value": {
              "type": "PropertySet",
              "properties": [
                {
                  "type": "PropertyDeclaration",
                  "name": "font-size",
                  "value": {
                    "type": "CSSVariable",
                    "name": "--fontSize"
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "width",
                  "value": {
                    "type": "CSSVariable",
                    "name": "--buttonWidth"
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "border",
                  "value": {
                    "type": "Keyword",
                    "value": "none"
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "color",
                  "value": {
                    "type": "Keyword",
                    "value": "white"
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "padding",
                  "value": {
                    "type": "Keyword",
                    "value": "8px 16px"
                  }
                },
                {
                  "type": "PropertyDeclaration",
                  "name": "cursor",
                  "value": {
                    "type": "Keyword",
                    "value": "pointer"
                  }
                }
              ]
            }
          },
          {
            "name": "textContent",
            "value": {
              "type": "StringLiteral",
              "value": "Click me"
            }
          }
        ]
      }
    },
    {
      "type": "ComponentDefinition",
      "name": "web-page",
      "template": {
        "type": "HTMLElement",
        "name": "my-button",
        "attributes": [
          {
            "name": "label",
            "value": {
              "type": "StringLiteral",
              "value": "Click me"
            }
          }
        ]
      }
    }
  ]
}

@easylogic
Copy link
Author

SaLang 기본 문법

/* 함수 정의 */
@function toggle(param) @js {
  return !param;
}

/* 컴포넌트 정의 */
@component my-button {
  /* 속성 정의 */
  @attribute {
    label: "Click me";
  }

  /* 상태 정의 */
  @state {
    --isActive: false;
  }

  /* 이벤트 핸들러 */
  @event click @js {
    this {
      --isActive: toggle(var(--isActive)); // 함수를 사용하여 상태를 토글
      background-color: var(--isActive ? var(--primaryColor) : var(--secondaryColor));
      animation: fade-in var(--animationDuration);
    }
  }

  /* 애니메이션 정의 */
  @keyframes fade-in {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  /* 조건부 렌더링 */
  @if (--isActive) {
    display: block;
  } @else {
    display: none;
  }

  /* 컴포넌트 내부 요소 */
  @template {
    button {
      font-size: var(--fontSize);
      width: var(--buttonWidth);
      border: none;
      color: white;
      padding: 8px 16px;
      cursor: pointer;
    }
  }
}

/* 웹 페이지 정의 */
@component web-page {
  /* 웹 페이지 스타일 */
  @template {
    my-button(label: "Click me") { }
  }
}

@easylogic
Copy link
Author

새롭게 만들어진 언어 스펙

/* 외부 컴포넌트 및 스타일 불러오기 */
@import './components/reusable-section.slg';
@import 'my-ui-library/reusable-style';
@import 'my-ui-library/button-component';

/* 전역 스타일 정의 */
@global-style {
  primaryColor: #007bff;
  secondaryColor: #6c757d;
  fontSize: 16px;
  buttonWidth: 100px;
}

/* 커스텀 버튼 컴포넌트 정의 */
@component custom-button-component {
  @attribute label: "Custom Button";
  @shadow-dom { encapsulated: true }

  @template-style(diff) {
    button {
      content: attr(label);
      font-size: var(fontSize);
      width: var(buttonWidth);
      border: none;
      color: white;
      padding: 8px 16px;
      background-color: var(secondaryColor);

      &:hover {
        background-color: var(primaryColor);
      }
    }
  }

  @event click {
    console.log("Custom button clicked!");
  }
}

/* 웹 페이지 컴포넌트 정의 */
@component web-page {
  @template {
    reusable-section { }
    custom-button-component(label: "Click Me") { }
    button-component(label: "Imported Button") { }
  }
}

@easylogic
Copy link
Author

easylogic commented Dec 18, 2023

/* 전역 스타일 및 테마 정의 */
@global-style {
  primaryColor: #007bff;
  secondaryColor: #6c757d;
  fontSize: 16px;
  buttonWidth: 100px;
}

/* 버튼 컴포넌트 정의 */
@component button-component {
  @attribute label: "Click me";
  @shadow-dom { encapsulated: true }

  /* 버튼의 HTML과 스타일 정의, Diff 활성화 */
  @template-style(diff) {
    button {
      content: attr(label);
      font-size: var(fontSize);
      width: var(buttonWidth);
      border: none;
      color: white;
      padding: 8px 16px;
      background-color: var(secondaryColor);
      cursor: pointer;

      &:hover {
        background-color: var(primaryColor);
      }
    }
  }

  @event click {
    console.log("Button clicked!");
  }
}

/* 슬롯 및 @@children 사용 컴포넌트 */
@component content-box {
  @template {
    div {
      @@children;
    }
  }
}

/* 멀티라인 텍스트를 <pre> 태그로 표현 */
@component multi-line-text-component {
  @template {
    pre {
      """
      이것은 멀티라인 텍스트입니다.
      여러 줄에 걸쳐 텍스트를 표현할 수 있으며,
      <pre> 태그를 사용하여 텍스트의 서식을 유지합니다.
      """
    }
  }
}

/* 반응형 리스트 컴포넌트 */
@component responsive-list {
  @state items: [];
  @media(max-width: 600px) { style: { list-style: "none" } }

  @template(diff) {
    ul {
      for item in var(items) where item > 2 {
        li { content: "필터링된 값: " + item }
      }
    }
  }
}

/* 포함(include)을 사용한 컴포넌트 */
@component reusable-section {
  @template {
    div {
      "이것은 재사용 가능한 섹션입니다."
    }
  }
}

@component web-page {
  @template {
    button-component(label: "Press Me") { }
    content-box {
      "직접 추가된 컨텐츠";
      button-component(label: "Another Button");
    }
    multi-line-text-component { }
    responsive-list { }
  }
}

@easylogic
Copy link
Author

PEG 문법

Start
  = Component+

Component
  = "@component" _ identifier _ "{" _ ComponentBody _ "}"

ComponentBody
  = (Attribute / Style / Event / Template)*

Attribute
  = "@attribute" _ identifier ":" _ StringLiteral _ ";"

Style
  = "@template-style(" _ diff:("diff")? _ ")" _ "{" _ CSSRules _ "}"

Event
  = "@event" _ identifier _ "{" _ JavaScriptCode _ "}"

Template
  = "@template" _ "{" _ TemplateContent _ "}"

TemplateContent
  = Element+

Element
  = TagName _ "{" _ (Text / Element)* _ "}"

TagName
  = identifier

Text
  = [^{}]+ // 중괄호 외부의 텍스트를 처리

CSSRules
  = CSSRule+

CSSRule
  = selector _ "{" _ CSSProperties _ "}"

CSSProperties
  = CSSProperty+

CSSProperty
  = property _ ":" _ value _ ";"

// 유틸리티 규칙 정의
_ "whitespace"
  = [ \t\n\r]*

identifier "identifier"
  = [a-zA-Z_][a-zA-Z0-9_]*

StringLiteral "string"
  = "\"" [^\"]* "\""

JavaScriptCode
  = "{" _ [^}]* "}" // 기본적인 JavaScript 코드 처리

selector "CSS selector"
  = [^{}]+

property "CSS property"
  = [^:]+

value "CSS value"
  = [^;]+

@easylogic
Copy link
Author

/* 외부 컴포넌트 및 스타일 불러오기 */
@import 'my-ui-library/button-component';
@import 'my-ui-library/responsive-style';

/* 전역 스타일 정의 */
@global-style {
  primaryColor: #007bff;
  secondaryColor: #6c757d;
  fontSize: 16px;
  buttonWidth: 100px;
}

/* 접근성을 고려한 버튼 컴포넌트 정의 */
@component custom-button-component extends button {
  @attribute label: "Custom Button";
  @attribute ariaLabel: "Custom Accessible Button";
  @attribute role: "button";

  /* 스타일 정의 */
  @template-style(diff) {
    button(aria-label: var(ariaLabel), role: var(role)) {
      content: var(label);
      font-size: var(fontSize);
      width: var(buttonWidth);
      border: none;
      color: white;
      padding: 8px 16px;
      background-color: var(secondaryColor);

      &:hover {
        background-color: var(primaryColor);
      }
    }
  }

  /* 이벤트 처리 */
  @event click {
    console.log("Custom button clicked!");
  }
}

/* 멀티라인 텍스트 컴포넌트 */
@component multi-line-text-component {
  @template {
    pre {
      """
      여러 줄에 걸친 텍스트입니다.
      <pre> 태그 내에서 텍스트의 서식을 유지합니다.
      """
    }
  }
}

/* 반응형 리스트 컴포넌트 */
@component responsive-list {
  @state items: [];
  @media(max-width: 600px) { style: { list-style: "none" } }

  /* 리스트 아이템 정의 */
  @template(diff) {
    ul {
      for item in var(items) where item > 2 {
        li { content: "필터링된 값: " + item }
      }
    }
  }
}

/* 웹 페이지 컴포넌트 정의 */
@component web-page {
  @template {
    custom-button-component(label: "Click Me") { }
    multi-line-text-component { }
    responsive-list { }
  }
}

@easylogic
Copy link
Author

SaLang 업데이트

@component MyComponent {
  @attribute title: "My Amazing Component";
  @attribute isVisible: true;

  @style {
    .my-component {
      color: var(primaryColor);
      padding: 10px;
    }

    .hidden {
      display: none;
    }
  }

  @function toggleVisibility() {
    @js {
      this.isVisible = !var(isVisible);
    }
  }

  @event click {
    @selector .toggle-btn {
      background-color: var(secondaryColor);
    }
    @js {
      toggleVisibility();
    }
  }

  @template {
    div(class: "my-component" var(isVisible ? '' : ' hidden')) {
      "Title: " var(title)

      @if var(isVisible) {
        div { "This content is visible" }
      } @else {
        div { "This content is hidden" }
      }

      @repeat item in var(items) {
        @if var(item.isVisible) {
          div(class: "item") { "Item: " var(item.name) }
        } @else {
          div(class: "item hidden") { "Hidden Item" }
        }
      }

      button(class: "toggle-btn") { "Toggle Visibility" }
    }
  }
}

@easylogic
Copy link
Author

easylogic commented Dec 19, 2023

PEG Parser 업데이트

{
  // Helper function to remove whitespace nodes
  function filterWhitespace(nodes) {
    return nodes.filter(node => {
      if (typeof node === 'string') return node.trim() !== '';
      return node;
    });
  }

  // Helper function to create a text node
  function createTextNode(text) {
    return { type: 'TextNode', value: text.trim() };
  }
}

// The entry point of the grammar
Start
  = components:Component+ { return components; }

// Defines a component
Component
  = "@component" _ id:identifier _ "{" _ body:ComponentBody _ "}" {
      return { type: "Component", id, body };
    }

// Body of a component, can contain various types of content
ComponentBody
  = content:(_ Attribute _ / _ Event _ / _ Style _ / _ Function _ / _ Template _)* {
      return filterWhitespace(content.flat(Infinity));
    }

// Defines an attribute of a component
Attribute
  = "@attribute" _ id:identifier ":" _ value:attributeValue _ ";" {
      return { type: "Attribute", id, value };
    }

// Possible values for an attribute
attributeValue
  = StringLiteral / NumberLiteral / BooleanLiteral / identifier

// Defines an event handler
Event
  = "@event" _ eventName:identifier _ eventArgs:EventArgs? _ "{" _ eventBody:EventBody _ "}" {
      return { type: "Event", eventName, args: eventArgs || [], body: eventBody };
    }

// Event arguments
EventArgs
  = "(" _ argList:identifierList _ ")" {
      return argList;
    }

// List of identifiers, used for event arguments
identifierList
  = first:identifier _ rest:("," _ identifier)* {
      return [first, ...rest.map(r => r[2])];
    }

// Body of an event, can contain selectors and JavaScript sections
EventBody
  = sections:(_ SelectorRule _ / _ JavaScriptSection _)* {
      return filterWhitespace(sections.flat(Infinity));
    }

// JavaScript code section within an event
JavaScriptSection
  = "@js" _ "{" _ code:JavaScriptCode _ "}" {
      return { type: "JavaScript", code };
    }

// JavaScript code, can be a mix of text and variables
JavaScriptCode
  = code:(_ JavaScriptCodeChunk _)* {
      return filterWhitespace(code.flat(Infinity)).map(it => {
        if (typeof it === 'string') return { type: 'JavaScriptCodeChunk', value: it.trim() };
        return it;
      });
    }

// Chunk of JavaScript code, either a variable or plain text
JavaScriptCodeChunk
  = Variable / NonVariableChunk

// Plain text chunk within JavaScript code
NonVariableChunk
  = chars:[^{}]+ {
      return chars.join('');
    }

// Selector rule within an event or function
SelectorRule
  = "@selector" _ selector:selector _ "{" _ properties:CSSPropertiesWithVars _ "}" {
      return { type: "CSSRule", selector, properties };
    }

// CSS properties, allowing variables
CSSPropertiesWithVars
  = properties:(_ CSSPropertyWithVar _ / _ CSSProperty _)* {
      return filterWhitespace(properties.flat(Infinity));
    }

// CSS property that includes a variable
CSSPropertyWithVar
  = property:property _ ":" _ value:Variable _ ";" {
      return { type: "CSSPropertyWithVar", property, value };
    }

// Variable within CSS or JavaScript
Variable
  = "var(" _ varName:identifier _ ")" {
      return { type: "Variable", varName };
    }

// Defines a function
Function
  = "@function" _ functionName:identifier _ functionArgs:FunctionArgs? _ "{" _ functionBody:FunctionBody _ "}" {
      return { type: "Function", functionName, args: functionArgs ? functionArgs : [], body: functionBody };
    }

// Arguments of a function
FunctionArgs
  = "(" _ argList:identifierList _ ")" {
      return argList;
    }

// Body of a function, can contain selectors and JavaScript sections
FunctionBody
  = sections:( _ SelectorRule _ / _ JavaScriptSection _)* {
      return filterWhitespace(sections.flat(Infinity));
    }

// Defines CSS styles
Style
  = "@style" _ "{" _ rules:CSSRules _ "}" {
      return { type: "Style", rules };
    }

// Template of a component
Template
  = "@template" _ "{" _ content:TemplateContent _ "}" {
      return { type: "Template", content: filterWhitespace(content.flat(Infinity)) };
    }

// Content of a template, including conditional rendering, repeat rendering, elements, and text
TemplateContent
  = elements:(_ ConditionalRendering _ / _ RepeatRendering _ / _ Element _ / _ Text _ )* { 
    return filterWhitespace(elements.flat(Infinity)); 
}

// Conditional rendering rules
ConditionalRendering
  = _ ifContent:IfConditionalRendering _ elseContent:ElseConditionalRendering? _  {
      return { type: "Conditional", ifContent, elseContent };
    }

// 'If' part of conditional rendering
IfConditionalRendering
  = "@if" _ condition:JavaScriptCode _ "{" _ content:TemplateContent _ "}" {
      return { type: "IfConditional", condition, content };
    }

// 'Else' part of conditional rendering
ElseConditionalRendering
  = "@else" _ "{" _ content:TemplateContent _ "}" {
      return { type: "ElseConditional", content };
    }    

// Repeat rendering rule
RepeatRendering
  = "@repeat" _ variable:identifier _ "in" _ collection:JavaScriptCode _ "{" _ content:TemplateContent _ "}" {
      return { type: "Repeat", variable, collection, content };
    }

// HTML element
Element
  = tag:TagName _ "{" _ content:(_ Element _ / _ Text _)* _ "}" {
      return { type: "Element", tag, content: filterWhitespace(content.flat(Infinity)) };
    }

// Tag name of an HTML element
TagName
  = id:identifier { return id; }

// Text node
Text
  = chars: [^{}\n]+ { return createTextNode(chars.join('')); }  

// CSS rules
CSSRules
  = rules:CSSRule* { return rules; }

// Single CSS rule
CSSRule
  = selector:selector _ "{" _ properties:CSSProperties _ "}" {
      return { type: "CSSRule", selector, properties };
    }

// CSS properties
CSSProperties
  = properties:CSSProperty* { return properties; }

// Single CSS property
CSSProperty
  = property:property _ ":" _ value:value _ ";" {
      return { type: "CSSProperty", property, value };
    }

// Whitespace
_ "whitespace"
  = [ \t\n\r]*

// Identifier (includes complex identifiers with dot notation)
identifier "identifier"
  = head:[a-zA-Z_] tail:[a-zA-Z0-9_.]* { return head + tail.join(''); }

// String literal
StringLiteral "string"
  = DoubleQuotedString / SingleQuotedString / BacktickQuotedString / TripleDoubleQuotedString
  
DoubleQuotedString "double quoted string"  
  = "\"" chars:[^\"]* "\"" { return chars.join(''); }

SingleQuotedString "single quoted string"
  = "'" chars:[^']* "'" { return chars.join(''); }

BacktickQuotedString "backtick quoted string"
  = '`' chars:[^`]* '`' { return chars.join(''); }

TripleDoubleQuotedString "triple double quoted string"
  = '"""' chars:(TripleDoubleQuotedChar*) '"""' { return chars.join(''); }

TripleDoubleQuotedChar
  = !'"""' char:. { return char; }

// Number literal
NumberLiteral "NumberLiteral"
  = digits:[0-9]+ { return parseInt(digits.join('')); }

// Boolean literal
BooleanLiteral "BooleanLiteral"
  = "true" { return true; }
  / "false" { return false; }

// CSS selector
selector "CSS selector"
  = chars:[^{]+ { return chars.join('').trim(); }

// CSS property
property "CSS property"
  = chars:[^:]+ { return chars.join('').trim(); }

// CSS value
value "CSS value"
  = chars:[^;]+ { return chars.join('').trim(); }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment