{"id":24745756,"url":"https://github.com/ertgl/cx-tagged-template","last_synced_at":"2025-03-23T00:21:53.095Z","repository":{"id":274517333,"uuid":"917814838","full_name":"ertgl/cx-tagged-template","owner":"ertgl","description":"Class-name expressions in the style of concatenative programming.","archived":false,"fork":false,"pushed_at":"2025-03-17T13:36:21.000Z","size":1996,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-20T00:54:52.115Z","etag":null,"topics":["class-names","concatenative","css","css-modules","domain-specific-language","forth","javascript","jsx","tagged-template","typescript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ertgl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-16T17:39:52.000Z","updated_at":"2025-03-17T13:36:17.000Z","dependencies_parsed_at":"2025-01-27T20:04:17.233Z","dependency_job_id":"d6a4c587-77a9-4f4b-a578-a15e58a7bc7a","html_url":"https://github.com/ertgl/cx-tagged-template","commit_stats":null,"previous_names":["ertgl/cx-tagged-template"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Fcx-tagged-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Fcx-tagged-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Fcx-tagged-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ertgl%2Fcx-tagged-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ertgl","download_url":"https://codeload.github.com/ertgl/cx-tagged-template/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245037435,"owners_count":20550870,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["class-names","concatenative","css","css-modules","domain-specific-language","forth","javascript","jsx","tagged-template","typescript"],"created_at":"2025-01-28T03:29:36.953Z","updated_at":"2025-03-23T00:21:53.080Z","avatar_url":"https://github.com/ertgl.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cx-tagged-template\n\nSpecification and initial implementation of a sophisticated\nclass-name-expression DSL, written in TypeScript.\n\n\u003cpicture\u003e\n  \u003csource\n    media=\"(prefers-color-scheme: dark)\"\n    srcset=\"./assets/jsx-sample-dark.png\"\n  /\u003e\n  \u003cimg\n    alt=\"Using cx-tagged-template with JSX\"\n    src=\"./assets/jsx-sample-light.png\"\n  /\u003e\n\u003c/picture\u003e\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Installation](#installation)\n- [Usage](#usage)\n- [Examples](#examples)\n- [Syntax and Semantics](#syntax-and-semantics)\n  - [Consolidator](#consolidator)\n  - [Tokenizer](#tokenizer)\n  - [Parser](#parser)\n  - [Stack](#stack)\n  - [Operators](#operators)\n    - [Built-in Operators](#built-in-operators)\n    - [Implicit Operators](#implicit-operators)\n  - [Interpreter](#interpreter)\n  - [Transformer](#transformer)\n  - [Renderer](#renderer)\n  - [Template Tag](#template-tag)\n- [References](#references)\n- [License](#license)\n\n## Overview\n\nCX (class-expressions) is a concatenative domain-specific language for\nconstructing class-name expressions with a minimal, yet, expressive syntax.\nThe language is initially designed to be used with tagged template literals in\nJavaScript, but it can also be implemented in other languages that support\nuser-defined\n[sigils](https://en.wikipedia.org/wiki/Sigil_(computer_programming)),\n[macros](https://en.wikipedia.org/wiki/Macro_(computer_science)),\nor other forms of syntactic extensions.\n\n## Installation\n\nThe package `cx-tagged-template` is available on `npm` and can be installed\nusing any compatible package manager.\n\n```sh\nnpm install cx-tagged-template\n```\n\nThe code is compiled to both CJS and ESM formats, and supports tree-shaking.\nWhen bundled, the code size can be reduced to approximately 2.17 KB (minified\nand gzipped).\n\n## Usage\n\nTo start using the `cx` template tag, you can import it from the package:\n\n```js\nimport { cx } from 'cx-tagged-template';\n```\n\nThen, you can use the `cx` template tag to create class-name expressions:\n\n```js\nconst className = cx`nice nice--better ${0} nice--best`; // \"nice--best\"\n```\n\n## Examples\n\nThese code snippets demonstrate various features of the `cx` tagged template in\nJavaScript:\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Using non-string values as conditional expressions\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  Non-string values can be used as condition tests in class-name expressions.\n  When a non-string value is used as an interpolation, it will be evaluated as\n  a conditional expression, like the `if` statements. If the value is truthy,\n  the preceding values will be kept in the stack for the next operations.\n  Otherwise, the values will be removed from the stack.\n\n  Separating the non-string values from the preceding values with whitespaces\n  is not necessary. However, it is recommended for better readability.\n\n  ```js\n  const bordered = false;\n\n  cx`\n  nice ${!bordered}\n  bordered ${bordered}\n  `;\n\n  // Output: \"nice\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Using interpolations for string concatenations\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  String values can be embedded in the class-name expressions by using\n  interpolations. When a placeholder is used with a string value and it is\n  not separated by whitespaces, it will be concatenated with the preceding\n  values.\n\n  ```js\n  const colors = {\n    dark: {\n      fg: \"white\",\n    },\n    light: {\n      fg: \"black\",\n    },\n  };\n\n  cx`text-${colors.light.fg} dark:text-${colors.dark.fg}`;\n\n  // Output: \"text-black dark:text-white\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Using interpolations for dynamic class-names\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  String interpolations that are separated by whitespaces can be used to create\n  dynamic class-names (e.g. based on variables or JavaScript expressions).\n\n  ```js\n  /**\n   * @type {\"\" | \"column\" | \"row\"}\n   */\n  const flexDirection = \"column\";\n\n  cx`flex ${flexDirection}`;\n\n  // Output: \"flex column\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Using string interpolations as conditional expressions\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  The `test` operator can be used for conditional removal of values based\n  on the last value in the stack. It is automatically inserted by the parser\n  when a non-string interpolation is detected. However, to test string values,\n  the operator can be used explicitly.\n\n  **Hint:** The `test` operator does not remove non-empty string test values.\n\n  ```js\n  /**\n   * @type {\"\" | \"column\" | \"row\"}\n   */\n  const flexDirection = \"\";\n\n  cx`\n  nice\n  flex ${flexDirection} ${cx.op.test} box\n  `;\n\n  // Output: \"nice box\"\n  ```\n\n  ```js\n  /**\n   * @type {\"\" | \"column\" | \"row\"}\n   */\n  const flexDirection = \"column\";\n\n  cx`\n  nice\n  flex ${flexDirection} ${cx.op.test} box\n  `;\n\n  // Output: \"nice flex column box\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Emitting values in the stack to the renderer explicitly\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  Emitting is the process of sending the values in the stack to the renderer.\n  Which ignores non-string values and guarantees that the string values are\n  included in the final output (unless they are transformed to empty strings in\n  the renderer layer by a user-defined transformation function). It also clears\n  the stack for the next operations.\n\n  When a line-feed or end-of-template is detected, the `emit` operator is\n  automatically inserted by the parser. However, it can also be used\n  explicitly.\n\n  **Hint:** Every line is isolated from the operators that are placed in the\n  other lines.\n\n  ```js\n  /**\n   * @type {\"\" | \"column\" | \"row\"}\n   */\n  const flexDirection = \"column\";\n\n  cx`\n  nice\n  flex ${flexDirection} ${cx.op.emit} box\n  `;\n\n  // Output: \"nice flex column box\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Discarding values from the stack\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  Sometimes, it may be necessary to disable some class-names temporarily. The\n  `discard` operator can be used to clear the stack. Besides debugging\n  purposes, it can also be used for comments.\n\n  **Hint:** To comment out specific parts of a line, the `discard` operator can\n  be used with the conjunction of the `emit` operator.\n\n  ```js\n  /**\n   * @type {\"\" | \"column\" | \"row\"}\n   */\n  const flexDirection = \"column\";\n\n  cx`\n  nice\n  flex ${flexDirection} ${cx.op.emit} Comment out. ${cx.op.discard} box\n  Your lovely important note. ${cx.op.discard}\n  `;\n\n  // Output: \"nice flex column box\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Deduplicating class-names\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  The class-names emitted to the renderer are deduplicated by default. This\n  behavior ensures that the same class-name is not repeated in the final\n  output.\n\n  ```js\n  cx`foo foo bar`;\n\n  // Output: \"foo bar\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Transforming class-names e.g. with CSS Modules\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  Transformer layer is an extension point that allows developers to customize\n  the final output of the class-names by defining their own transformation\n  functions. The transformer layer can be used to apply transformations such as\n  mapping CSS Modules, adding prefixes or suffixes, or even removing class-names\n  based on certain conditions by returning an empty string.\n\n  For utilizing CSS Modules, the implementation provides a built-in extension\n  that can be used to create a transformer that maps class-names to their\n  respective values, which are imported from the CSS module file.\n\n  **Hint:** A custom `cx` tag can be created per CSS module file.\n\n  **Hint:** The custom `cx` tag can be named as `cmx` for distinguishing it\n  from the default `cx` tag.\n\n  ```js\n  import { createCX } from \"cx-tagged-template\";\n  import { createCSSModulesTransformer } from \"cx-tagged-template/extensions/css-modules\";\n\n  import styles from \"./styles.module.css\";\n\n  const cmx = createCX({\n    transformer: createCSSModulesTransformer(styles),\n  });\n\n  const className = cmx`foo bar`;\n\n  // Assuming that `styles.foo` equals to \"bar\", output: \"bar\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Defining custom operators\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  In addition to the built-in operators, custom operators can be defined by\n  using the `defineOperator` function. The operator function should accept the\n  stack and emit function as arguments.\n\n  This feature can be used for creating custom operators that are specific to\n  the project or the use-case.\n\n  In the following example, a custom operator named `prefix` is defined. The\n  operator accepts the last value as a prefix and prefixes the class-names that\n  are placed before the prefix.\n\n  **Hint:** The `cx.op` object can be used for registering and accessing the\n  operators. This eliminates the need for importing the operators in every\n  file.\n\n  **Hint:** Each custom `cx` tag can have its own set of custom operators.\n\n  ```js\n  cx.op.prefix = defineOperator({\n    name: \"prefix\",\n    operate(\n      stack,\n      emit,\n    )\n    {\n      // Get the last value by removing it from the stack.\n      const prefix = stack.values.pop();\n\n      // Keep the CX runtime error-free:\n      // Ignore the values that the operator cannot be applied to.\n      if (typeof prefix === \"string\")\n      {\n        // Iterate over the values in the stack.\n        for (let i = 0; i \u003c stack.values.length; i++)\n        {\n          // Get the value of the current index.\n          const value = stack.values[i];\n\n          // Keep the CX runtime error-free:\n          // Ignore the values that the operator cannot be applied to.\n          if (typeof value === \"string\")\n          {\n            // Mutate the value in the stack.\n            stack.values[i] = `${prefix}${value}`;\n          }\n        }\n      }\n    },\n  });\n\n  cx`foo bar the- ${cx.op.prefix}`;\n\n  // Output: \"the-foo the-bar\"\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cstrong\u003e\n      Using with JSX components\n    \u003c/strong\u003e\n  \u003c/summary\u003e\n\n  The `cx` tagged template can be used inside JSX components for creating\n  dynamic class-names, with an increased level of readability and\n  maintainability compared to the traditional string concatenation methods.\n\n  ```jsx\n  const Button = (props) =\u003e\n  {\n    const {\n      bordered = false,\n      className,\n      color = \"primary\",\n      dense = false,\n      disabled = false,\n      ...rest\n    } = props;\n\n    return (\n      \u003cbutton\n        className={cmx`\n          cta-button\n          cta-button--${color}\n          px-4 py-1.5 ${!dense}\n          border\n          ${bordered ? \"border-gray-300 dark:border-gray-700\" : \"border-transparent\"}\n          opacity-50 cursor-not-allowed ${disabled}\n          Append custom class-names passed from the parent component: ${cx.op.discard}\n          ${className}\n        `}\n        {...rest}\n      /\u003e\n    );\n  };\n  ```\n\u003c/details\u003e\n\n## Syntax and Semantics\n\nThe syntax and semantics of CX are designed to be minimal and easy to use,\nallowing developers to create class-name expressions that are both dynamic and\ncomposable. At the early stages of CX's design process, the language was not\nactually inspired by concatenative programming concepts. However, as it\nevolved, I found myself aligning with the principles of concatenative\nprogramming languages and decided to embrace them. Since CX focuses only on\nclass-name expressions, it is tuned to be more developer-friendly on this\nspecific purpose. For example, in CX, line-feeds are considered as emit\noperators, and non-string interpolations are considered as test operators.\nThis design choice provides a smooth developer experience by reducing\nkeystrokes, boilerplate codes, and cognitive load; while increasing the\nexpressiveness. Apart from these differences, CX's syntax and semantics can be\nconsidered as a subset of other concatenative programming languages like Forth.\n\nBefore diving into the implementation details, it is worth mentioning that;\nfor ensuring the compatibility and correctness of the implementation with the\nreal-world class-names, the implementation has been thoroughly tested with over\n21400 scenarios using a small dataset of class-names composed with different\nsyntaxes and patterns. The dataset is built by extracting various class-names\nfrom the documentation of one of the most popular CSS frameworks,\n`Tailwind CSS`.\n\nTo understand the syntax and semantics of CX, let's continue with this\nJavaScript implementation; as it can also be used as a reference for integrating\nthe DSL into other languages. The implementation of `cx-tagged-template` is\ncomposed of several key components, each responsible for a specific aspect of\nthe class-name-expression construction process.\n\nIn the following sections, we will explore each of these components in detail,\nstarting with the consolidator layer, which is responsible for transforming\ntagged-template specific data into a more generalized format.\n\n### Consolidator\n\nThe consolidator is tasked with combining template strings and expressions into\na unified stream of fragments, ensuring the correct order and type of\nfragments.\n\nFor example, given the following template:\n\n```js\ncx`nice ${false} nice--better ${true}`;\n```\n\nThe consolidator emits the following fragments to the parser:\n\n```js\n{ index: 0, type: 'template-string', value: 'nice ' }\n{ index: 0, type: 'template-expression', value: false }\n{ index: 1, type: 'template-string', value: ' nice--better ' }\n{ index: 1, type: 'template-expression', value: true }\n{ index: 3, type: 'template-feed', value: '' }\n```\n\n### Tokenizer\n\nThe tokenizer parses template-string fragments into tokens, which represent the\nsmallest units of the language.\n\nAs the fragments are received by the parser, parser may use the tokenizer to\nconvert string fragments into tokens. For the given fragments above, the\ntokenizer emits the following tokens back to the parser:\n\n```js\n{ index: 0, type: 'character', value: 'n' }\n{ index: 1, type: 'character', value: 'i' }\n{ index: 2, type: 'character', value: 'c' }\n{ index: 3, type: 'character', value: 'e' }\n{ index: 4, type: 'whitespace', value: ' ' }\n{ index: 5, type: 'eof', value: '' }\n{ index: 0, type: 'whitespace', value: ' ' }\n{ index: 1, type: 'character', value: 'n' }\n{ index: 2, type: 'character', value: 'i' }\n{ index: 3, type: 'character', value: 'c' }\n{ index: 4, type: 'character', value: 'e' }\n{ index: 5, type: 'character', value: '-' }\n{ index: 6, type: 'character', value: '-' }\n{ index: 7, type: 'character', value: 'b' }\n{ index: 8, type: 'character', value: 'e' }\n{ index: 9, type: 'character', value: 't' }\n{ index: 10, type: 'character', value: 't' }\n{ index: 11, type: 'character', value: 'e' }\n{ index: 12, type: 'character', value: 'r' }\n{ index: 13, type: 'whitespace', value: ' ' }\n{ index: 14, type: 'eof', value: '' }\n```\n\n### Parser\n\nThe parser performs both syntactic and semantic analysis of the fragments and\ntokens. It evaluates string interpolations and inserts implicit operators as\nnecessary.\n\nContinuing the examples in the previous sections, the parser emits the\nfollowing values and operators to the interpreter:\n\n```js\nnice\nfalse\n[Function: operate] { [Symbol(__cx_operator__)]: { name: 'test' } }\nnice--better\ntrue\n[Function: operate] { [Symbol(__cx_operator__)]: { name: 'test' } }\n[Function: operate] { [Symbol(__cx_operator__)]: { name: 'emit' } }\n```\n\n### Stack\n\nThe stack serves as the storage layer for the interpreter, buffering\nclass-names and other values between the interpreter and the renderer.\n\n### Operators\n\nOperators are functions that modify the stack based on their specific behavior.\nBuilt-in operators handle tasks such as emitting class-names to the renderer or\nremoving values from the stack, e.g.: conditional removal, etc.\n\nTo be compatible with the CSS selector syntax, all the punctuation characters\nare allowed in the string fragments. Because of this, the language should not\nhave any operators that can be used outside of interpolation placeholders\n(expression fragments). Besides the compatibility, this design choice also\nhelps reducing the cognitive load by making the language more predictable and\neasier to use.\n\nFor example, in the following snippet, the `discard` operator can be seen as an\ninterpolation, which is specified inside a placeholder.\n\n```js\ncx`nice nice--better ${true} ${cx.op.discard} nice--best`; // \"nice--best\"\n```\n\n#### Built-in Operators\n\nRespecting the minimalist nature of the language, a small set of operators is\nprovided to handle the most essential tasks. These operators are:\n\n- [`discard`](./src/operators/discard.ts): Clears the stack. It can be used for\n  comments or debugging purposes.\n- [`emit`](./src/operators/emit.ts): Emits the string values in the stack to\n  the renderer, then clears the stack for the next operations.\n- [`test`](./src/operators/test.ts): Works as a conditional operator, removing\n  values from the stack based on the last value.\n\n#### Implicit Operators\n\nImplicit operators are automatically inserted by the parser to handle\npredefined behaviors. For example, the `test` operator is inserted when a\nconditional expression (non-string and non-operator value) is detected. And the\n`emit` operator is inserted when a line feed or template feed (end of template)\nis detected.\n\n### Interpreter\n\nThe interpreter manages the stack, pushing values onto it and executing\noperators as required.\n\nBack on the example in the parser section, as the parser emits the values and\noperators, the interpreter processes them, updating the stack accordingly. Each\nline in this demonstration represents the state of the stack after a value or\noperator is processed:\n\n```js\nStack: []\nStack: [\"nice\"]\nStack: [\"nice\", false]\nOperation: test\nStack: []\nStack: [\"nice--better\"]\nStack: [\"nice--better\", true]\nOperation: test,\nStack: [\"nice--better\"]\nOperation: emit,\nStack: []\n```\n\n### Transformer\n\nAs an extension point, transformers allow developers to customize the final\noutput of the class-names by defining their own transformation functions. The\ntransformer layer can be used to apply transformations such as\n[CSS Modules](https://github.com/css-modules/css-modules), adding prefixes or\nsuffixes, or even removing class-names based on certain conditions by returning\nan empty string.\n\n### Renderer\n\nThe renderer aggregates and deduplicates class-names, applying any specified\ntransformation. It concatenates the class-names into a single string, which\nis returned to the template tag.\n\nOnce a class-name is emitted to the renderer; unless it is transformed to an\nempty string by a user-defined transformer, it is guaranteed to be uniquely\npresent in the final output.\n\n### Template Tag\n\nThe template tag serves as the public interface, allowing developers to create\nclass-name expressions. It orchestrates the flow of data through the various\ncomponents, ultimately returning the final result.\n\n## References\n\n- [Concatenative programming language - Wikipedia](https://en.wikipedia.org/wiki/Concatenative_programming_language)\n- [Tagged templates - JavaScript - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates)\n\n## License\n\nThis project is licensed under the\n[MIT License](https://opensource.org/license/mit).\nFor more information, see the [LICENSE](./LICENSE) file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fertgl%2Fcx-tagged-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fertgl%2Fcx-tagged-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fertgl%2Fcx-tagged-template/lists"}