From 50c8817c36fbdc897e79c7c143f3bacc46f4a293 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 6 Oct 2023 18:03:51 -0700 Subject: [PATCH] [First-Class Mixins] Flush to spec Closes #626 --- spec/at-rules/function.md | 28 ++++++++++-- spec/at-rules/mixin.md | 39 ++++++++++------ spec/built-in-modules.md | 38 ++++++++++++++++ spec/built-in-modules/meta.md | 86 +++++++++++++++++++++++++++++++++-- spec/types/functions.md | 62 +++++++++++++++++++++++++ spec/types/mixins.md | 63 +++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 23 deletions(-) create mode 100644 spec/built-in-modules.md create mode 100644 spec/types/functions.md create mode 100644 spec/types/mixins.md diff --git a/spec/at-rules/function.md b/spec/at-rules/function.md index 5d118072..aa3fd5c6 100644 --- a/spec/at-rules/function.md +++ b/spec/at-rules/function.md @@ -28,20 +28,38 @@ To execute a `@function` rule `rule`: [vendor prefix]: ../syntax.md#vendor-prefix +* Let `parent` be the [current scope]. + + [current scope]: ../spec.md#scope + +* Let `function` be a [function] named `name` which does the following when + executed with `args`: + + [function]: ../types/functions.md + + * With the current scope set to an empty [scope] with `parent` as its parent: + + * Evaluate `args` with `rule`'s `ArgumentDeclaration`. + + * Execute each statement in `rule`. + + * Return the value from the `@return` rule if one was executed, or throw an + error if no `@return` rule was executed. + + [scope]: ../spec.md#scope + * If `rule` is outside of any block of statements: * If `name` *doesn't* begin with `-` or `_`, set [the current module][]'s - function `name` to `rule`. + function `name` to `function`. > This overrides the previous definition, if one exists. - * Set [the current import context][]'s function `name` to `rule`. + * Set [the current import context][]'s function `name` to `function`. > This happens regardless of whether or not it begins with `-` or `_`. [the current module]: ../spec.md#current-module [the current import context]: ../spec.md#current-import-context -* Otherwise, set the [current scope]'s function `name` to `rule`. - - [current scope]: ../spec.md#scope +* Otherwise, set the [current scope]'s function `name` to `function`. diff --git a/spec/at-rules/mixin.md b/spec/at-rules/mixin.md index a142f620..d154d199 100644 --- a/spec/at-rules/mixin.md +++ b/spec/at-rules/mixin.md @@ -31,25 +31,41 @@ To execute a `@mixin` rule `rule`: * Let `name` be the value of `rule`'s `Identifier`. +* Let `parent` be the [current scope]. + + [current scope]: ../spec.md#scope + +* Let `mixin` be a [mixin] named `name` which accepts a content block if `rule` + contains a `@content` rule. To execute this mixin with `args`: + + [mixin]: ../types/mixins.md + + * With the current scope set to an empty [scope] with `parent` as its parent: + + * Evaluate `args` with `rule`'s `ArgumentDeclaration`. + + * Execute each statement in `rule`. + + [scope]: ../spec.md#scope + * If `rule` is outside of any block of statements: * If `name` *doesn't* begin with `-` or `_`, set [the current module]'s - mixin `name` to `rule`. + mixin `name` to `mixin`. > This overrides the previous definition, if one exists. - * Set [the current import context]'s mixin `name` to `rule`. + * Set [the current import context]'s mixin `name` to `mixin`. > This happens regardless of whether or not it begins with `-` or `_`. -* Otherwise, set the [current scope]'s mixin `name` to `rule`. + [the current module]: ../spec.md#current-module + [the current import context]: ../spec.md#current-import-context + +* Otherwise, set the [current scope]'s mixin `name` to `mixin`. > This overrides the previous definition, if one exists. -[the current module]: ../spec.md#current-module -[the current import context]: ../spec.md#current-import-context -[current scope]: ../spec.md#scope - ## `@include` ### Syntax @@ -72,15 +88,12 @@ To execute an `@include` rule `rule`: * Let `name` be `rule`'s `NamespacedIdentifier`. -* Let `mixin` be the result of [resolving a mixin][] named `name`. If this - returns null, throw an error. +* Let `mixin` be the result of [resolving a mixin] named `name`. If this returns + null, throw an error. [resolving a mixin]: ../modules.md#resolving-a-member -* Execute `rule`'s `ArgumentInvocation` with `mixin`'s `ArgumentDeclaration` in - `mixin`'s scope. - -* Execute each statement in `mixin`. +* Execute `mixin` with `rule`'s `ArgumentInvocation`. ## `@content` diff --git a/spec/built-in-modules.md b/spec/built-in-modules.md new file mode 100644 index 00000000..0fbe10e2 --- /dev/null +++ b/spec/built-in-modules.md @@ -0,0 +1,38 @@ +# Built-In Modules + +Sass provides a number of built-in [modules] that may be loaded from URLs with +the scheme `sass`. These modules have no extensions, CSS trees, dependencies, or +source files. Their canonical URLs are the same as the URLs used to load them. + +[modules]: modules.md#module + +## Built-In Functions and Mixins + +Each function and mixin defined in a built-in modules is specified with a +signature of the form + +
+[\] ArgumentDeclaration
+
+ +[\]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram + +followed by a procedure. It's available as a member (either function or mixin) +in the module whose name is the value of the ``. When it's executed +with `args`: + +* With an empty scope with no parent as the [current scope]: + + [current scope]: spec.md#scope + + * Evaluate `args` with the signature's `ArgumentDeclaration`. + + * Run the procedure, and return its result if this is a function. + +Built-in mixins don't accept content blocks unless explicitly specified +otherwise. + +By convention, in these procedures `$var` is used as a shorthand for "the value +of the variable `var` in the current scope". + +> In other words, `$var` is the value passed to the argument `$var`. diff --git a/spec/built-in-modules/meta.md b/spec/built-in-modules/meta.md index 06b495cf..57af84c0 100644 --- a/spec/built-in-modules/meta.md +++ b/spec/built-in-modules/meta.md @@ -5,6 +5,7 @@ This built-in module is available from the URL `sass:meta`. ## Table of Contents * [Functions](#functions) + * [`accepts-content()`](#accepts-content) * [`calc-name()`](#calc-name) * [`calc-args()`](#calc-args) * [`call()`](#call) @@ -12,19 +13,36 @@ This built-in module is available from the URL `sass:meta`. * [`feature-exists()`](#feature-exists) * [`function-exists()`](#function-exists) * [`get-function()`](#get-function) + * [`get-mixin()`](#get-mixin) * [`global-variable-exists()`](#global-variable-exists) * [`inspect()`](#inspect) * [`keywords()`](#keywords) * [`mixin-exists()`](#mixin-exists) * [`module-functions()`](#module-functions) + * [`module-mixins()`](#module-mixins) * [`module-variables()`](#module-variables) * [`type-of()`](#type-of) * [`variable-exists()`](#variable-exists) * [Mixins](#mixins) + * [`apply()`](#apply) * [`load-css()`](#load-css) ## Functions +### `accepts-content()` + +This is a new function in the `sass:meta` module. + +``` +accepts-content($mixin) +``` + +* If `$mixin` is not a [mixin], throw an error. + + [mixin]: ../types/mixins.md + +* Return whether `$mixin` accepts a content block as a SassScript boolean. + ### `calc-name()` ``` @@ -144,6 +162,31 @@ This function is also available as a global function named `get-function()`. * Return [`use`'s module][]'s function named `$name`, or throw an error if no such function exists. +### `get-mixin()` + +``` +get-mixin($name, $module: null) +``` + +* If `$name` is not a string, throw an error. + +* If `$module` is null: + + * Return the result of [resolving a mixin] named `$name`. If this returns + null, throw an error. + + [resolving a mixin]: ../modules.md#resolving-a-member + +* Otherwise: + + * If `$module` is not a string, throw an error. + + * Let `use` be the `@use` rule in [the current source file] whose namespace is + equal to `$module`. If no such rule exists, throw an error. + + * Return [`use`'s module]'s mixin named `$name`, or throw an error if no such + mixin exists. + ### `global-variable-exists()` ``` @@ -198,16 +241,14 @@ This function is also available as a global function named `mixin-exists()`. * If `$module` is null: - * Return whether [resolving a mixin][] named `$name` returns null. - - [resolving a mixin]: ../modules.md#resolving-a-member + * Return whether [resolving a mixin] named `$name` returns null. * Otherwise, if `$module` isn't a string, throw an error. -* Otherwise, let `use` be the `@use` rule in [the current source file][] whose +* Otherwise, let `use` be the `@use` rule in [the current source file] whose namespace is equal to `$module`. If no such rule exists, throw an error. -* Return whether [`use`'s module][] contains a mixin named `$name`. +* Return whether [`use`'s module] contains a mixin named `$name`. ### `module-functions()` @@ -225,6 +266,22 @@ This function is also available as a global function named `module-functions()`. * Return a map whose keys are the names of functions in [`use`'s module][] and whose values are the corresponding functions. +### `module-mixins()` + +This is a new function in the `sass:meta` module. + +``` +module-mixins($module) +``` + +* If `$module` is not a string, throw an error. + +* Let `use` be the `@use` rule in [the current source file] whose namespace is + equal to `$module`. If no such rule exists, throw an error. + +* Return a map whose keys are the quoted string names of mixins in + [`use`'s module] and whose values are the corresponding mixins. + ### `module-variables()` ``` @@ -261,6 +318,7 @@ This function is also available as a global function named `type-of()`. | Function | `"function"` | | List | `"list"` | | Map | `"map"` | + | Mixin | `"mixin"` | | Null | `"null"` | | Number | `"number"` | | String | `"string"` | @@ -288,6 +346,24 @@ This function is also available as a global function named `variable-exists()`. ## Mixins +### `apply()` + +``` +apply($mixin, $args...) +``` + +* If `$mixin` is not a [mixin], throw an error. + +* If the current `@include` rule has a `ContentBlock` and `$mixin` doesn't + accept a block, throw an error. + +* Execute `$mixin` with the `ArgumentInvocation` `(...$args)`. Treat the + `@include` rule that invoked `apply` as the `@include` rule that invoked + `$mixin`. + + > This ensures that any `@content` rules in `$mixin` will use `apply()`'s + > `ContentBlock`. + ### `load-css()` ``` diff --git a/spec/types/functions.md b/spec/types/functions.md new file mode 100644 index 00000000..0d6c842e --- /dev/null +++ b/spec/types/functions.md @@ -0,0 +1,62 @@ +# Functions + +## Table of Contents + +* [Types](#types) + * [Operations](#operations) + * [Equality](#equality) + * [Serialization](#serialization) + +## Types + +The value type known as a "function" is a procedure that takes an +`ArgumentInvocation` `args` and returns a SassScript value. Each function has a +string name. + +> The specific details of executing this procedure differ depending on where and +> how the function is defined. + +### Operations + +A function follows the default behavior of all SassScript operations, except +that equality is defined as below. + +#### Equality + +Functions use reference equality: two function values are equal only if they +refer to the exact same instance of the same procedure. + +> If the same file were to be imported multiple times, Sass would create a new +> function value for each `@function` rule each time the file is imported. +> Because a new function value has been created, although the name, body, and +> source span of a given function from the file would be the same between +> imports, the values would not be equal because they refer to different +> instances. Functions pre-defined by the Sass language are instatiated at most +> once during the entire evaluation of a program. +> +> As an example, if we declare two functions: +> +> ```scss +> @function foo() { +> @return red; +> } +> +> $a: meta.get-function(foo); +> +> @mixin foo { +> @return red; +> } +> +> $b: meta.get-mixin(foo); +> ``` +> +> Although every aspect of the two functions is the same, `$a != $b`, because +> they refer to separate function values. + +### Serialization + +To serialize a function value: + +* If the value is not being inspected, throw an error. + +* Otherwise, emit `'get-function("'`, then the function's name, then `'")'`. diff --git a/spec/types/mixins.md b/spec/types/mixins.md new file mode 100644 index 00000000..b390c444 --- /dev/null +++ b/spec/types/mixins.md @@ -0,0 +1,63 @@ +# Mixins + +## Table of Contents + +* [Types](#types) + * [Operations](#operations) + * [Equality](#equality) + * [Serialization](#serialization) + +## Types + +The value type known as a "mixin" is a procedure that takes an +`ArgumentInvocation` `args` and returns nothing. Each mixin has a string name +and a boolean that indicates whether or not it accepts a content block. + +> The specific details of executing this procedure differ depending on where and +> how the mixin is defined. A mixin will typically add nodes to the CSS +> stylesheet. + +### Operations + +A mixin follows the default behavior of all SassScript operations, except that +equality is defined as below. + +#### Equality + +Mixins use reference equality: two mixin values are equal only if they refer to +the exact same instance of the same procedure. + +> If the same file were to be imported multiple times, Sass would create a new +> mixin value for each `@mixin` rule each time the file is imported. Because a +> new mixin value has been created, although the name, body, and source span of +> a given mixin from the file would be the same between imports, the values +> would not be equal because they refer to different instances. Mixins +> pre-defined by the Sass language are instatiated at most once during the +> entire evaluation of a program. +> +> As an example, if we declare two mixins: +> +> ```scss +> @mixin foo { +> color: red; +> } +> +> $a: meta.get-mixin(foo); +> +> @mixin foo { +> color: red; +> } +> +> $b: meta.get-mixin(foo); +> ``` +> +> Although every aspect of the two mixins is the same, `$a != $b`, because they +> refer to separate mixin values. + +### Serialization + +To serialize a mixin value: + +* If the value is not being inspected, throw an error. + +* Otherwise, emit `'get-mixin("'`, then the mixin's name, then `'")'`.