Stage 2 Draft / August 21, 2023

String Dedent

1 Scope

This is the spec text of the String Dedent proposal in ECMAScript. String dedenting removes leading indentation from each line of a multiline string.

2 Properties of the String Constructor 22.1.2

2.1 String.dedent ( templateOrFn, ...substitutions )

Note 1

The String.dedent function may be called with either a template array and a variable number of substitutions, or it may be called with a function which itself takes a template array and a variable number of substitutions.

When called with a template and a variable number of substitutions, a string is returned which has common leading indentation removed from all lines with non-whitespace.

When called with a function, a closure is returned which wraps the function. When the closure is called with a template and variable number of substitutions, the common leading indentation of the template is removed and the result of calling the function with this new dedented template and substitutions is returned.

Common leading indentation is defined as the longest sequence of whitespace characters that match exactly on every line that contains non-whitespace. Empty lines and lines which contain only whitespace do not affect common leading indentation.

When removing the common leading indentation, lines which contain only whitespace will have all whitespace removed.

  1. If templateOrFn is not an object, then
    1. NOTE: This check is to allow future extensions with different input types.
    2. Throw a TypeError exception.
  2. If IsCallable(templateOrFn) is true, then
    1. Let tag be templateOrFn.
    2. Let closure be a new Abstract Closure with parameters (template, ...substitutions) that captures tag and performs the following steps when called:
      1. If template is not an object, then
        1. Throw a TypeError exception.
      2. Let R be the this value.
      3. Let dedented be ? DedentTemplateStringsArray(template).
      4. Let args be the list-concatenation of « dedented » and substitutions.
      5. Return ? Call(tag, R, args).
    3. Return CreateBuiltinFunction(closure, 1, "", « »).
  3. Let template be templateOrFn.
  4. Let dedented be ? DedentTemplateStringsArray(template).
  5. Return ? CookTemplateStringsArray(dedented, substitutions, cooked).
Note 2

The dedent function is intended for use as a tag function of a Tagged Template (13.3.11). When called as such, the first argument will be a well formed template object and the rest parameter will contain the substitution values.

2.2 DedentTemplateStringsArray ( template )

The abstract operation DedentTemplateStringsArray takes argument template (an Object) and returns either a normal completion containing an Array or a throw completion. It performs the following steps when called:

  1. Let realm be the current Realm Record.
  2. Let dedentMap be realm.[[DedentMap]].
  3. Let rawInput be ? Get(template, "raw").
  4. For each element e of the dedentMap, do
    1. If e.[[Raw]] is not empty and SameValue(e.[[Raw]], rawInput) is true, return e.[[Dedented]].
  5. Assert: dedentMap does not currently contain an entry for rawInput.
  6. Let raw be ? DedentStringsArray(rawInput).
  7. Let cookedArr be CreateArrayFromList(CookStrings(raw)).
  8. Let rawArr be CreateArrayFromList(raw).
  9. Perform ! DefinePropertyOrThrow(cookedArr, "raw", PropertyDescriptor { [[Value]]: rawArr, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }).
  10. Perform ! SetIntegrityLevel(rawArr, frozen).
  11. Perform ! SetIntegrityLevel(cookedArr, frozen).
  12. Append the Record { [[Raw]]: rawInput, [[Dedented]]: cookedArr } to dedentMap.
  13. Return cookedArr.

2.3 DedentStringsArray ( template )

The abstract operation DedentStringsArray takes argument template (an ECMAScript language value) and returns either a normal completion containing a List of Strings, or a throw completion. It performs the following steps when called:

  1. Let t be ? ToObject(template).
  2. Let len be ? LengthOfArrayLike(t).
  3. If len = 0, then
    1. NOTE: Well-formed template strings arrays always contain at least 1 string.
    2. Throw a TypeError exception.
  4. Let blocks be ? SplitTemplateIntoBlockLines(t, len).
  5. Perform ? EmptyWhiteSpaceLines(blocks).
  6. Perform ? RemoveOpeningAndClosingLines(blocks, len).
  7. Let common be DetermineCommonLeadingIndentation(blocks).
  8. Let count be the length of common.
  9. Let dedented be a new empty List.
  10. For each element lines of blocks, do
    1. Assert: lines is not empty, because SplitTemplateIntoBlockLines guarantees there is at least 1 line per block.
    2. Let out be "".
    3. Let index be 0.
    4. For each Record { [[String]], [[Newline]], [[LineEndsWithSubstitution]] } line of lines, do
      1. NOTE: The first line of each block does not need to be trimmed. It's either the opening line, or it's the continuation of a line after a substitution.
      2. If index = 0 or line.[[String]] is the empty String, let c be 0; else let c be count.
      3. Let strLen be the length of line.[[String]].
      4. Let trimmed be the substring of line.[[String]] from c to strLen.
      5. Set out to the string-concatenation of out, trimmed, and line.[[Newline]].
      6. Set index to index + 1.
    5. Append out to dedented.
  11. Return dedented.

2.4 CookStrings ( raw )

The abstract operation CookStrings takes argument raw (a List of Strings) and returns a List of either String or undefined. It performs the following steps when called:

  1. Let cooked be a new empty List.
  2. For each element str of raw, do
    1. Let parsed be ParseText(StringToCodePoints(str), CookStringsCharactersWithBackslash).
    2. Assert: parsed is a Parse Node.
    3. NOTE: Even though String.dedent can be manually invoked with a value, the CookStringsCharactersWithBackslash nonterminal matches every string, so the above parse is guaranteed to succeed.
    4. Let val be the TV of parsed as defined in 12.8.6.
    5. NOTE: val can actually be undefined if the raw string contains an invalid escape sequence.
    6. Append val to cooked.
  3. Return cooked.

2.4.1 The CookStrings Grammar

The following grammar is used by CookStrings. It is identical to TemplateCharacters except that it allows "${"" and "`" to appear without being preceded by a backslash, allows a backslash as the final character, and accepts the empty sequence. Every sequence of code points is matched by this grammar.

CookStringsCharactersWithBackslash :: CookStringsCharactersopt \opt CookStringsCharacters :: CookStringsCharacter CookStringsCharactersopt CookStringsCharacter :: $ [lookahead = {] ` TemplateCharacter

2.4.1.1 Static Semantics: TV

Editor's Note
The following lines are added to the definition of TV.

2.5 SplitTemplateIntoBlockLines ( template, len )

The abstract operation SplitTemplateIntoBlockLines takes arguments template (an ECMAScript language value) and len (a non-negative integer) and returns either a normal completion containing a List of Lists of Records with fields [[String]] (a String), [[Newline]] (a String), and [[LineEndsWithSubstitution]] (a Boolean), or a throw completion. It performs the following steps when called:

  1. Let blocks be a new empty List.
  2. Let index be 0.
  3. Repeat, while index < len,
    1. Let str be ? Get(template, ! ToString(𝔽(index))).
    2. If Type(str) is String, then
      1. Let lines be a new empty List.
      2. Let strLen be the length of str.
      3. Let start be 0.
      4. Let i be 0.
      5. Repeat, while i < strLen,
        1. Let c be the substring of str from i to i + 1.
        2. Let n be 1.
        3. If c is "\r" and i + 1 < strLen, then
          1. Let next be the substring of str from i + 1 to i + 2.
          2. If next is "\n", set n to 2.
        4. If c is matched by LineTerminator, then
          1. Let substr be the substring of str from start to i.
          2. Let newline be the substring of str from i to i + n.
          3. Append the Record { [[String]]: substr, [[Newline]]: newline, [[LineEndsWithSubstitution]]: false } to lines.
          4. Set start to i + n.
        5. Set i to i + n.
      6. Let tail be the substring of str from start to strLen.
      7. If index + 1 < len, let lineEndsWithSubstitution be true; else let lineEndsWithSubstitution be false.
      8. Append the Record { [[String]]: tail, [[Newline]]: "", [[LineEndsWithSubstitution]]: lineEndsWithSubstitution } to lines.
      9. Append lines to blocks.
    3. Else,
      1. Throw a TypeError exception.
    4. Set index to index + 1.
  4. Return blocks.

2.6 EmptyWhiteSpaceLines ( blocks )

The abstract operation EmptyWhiteSpaceLines takes argument blocks (a List of Lists of Records with fields [[String]] (a String), [[Newline]] (a String), and [[LineEndsWithSubstitution]] (a Boolean)). It performs the following steps when called:

  1. For each element lines of blocks, do
    1. Let lineCount be the length of lines.
    2. NOTE: We start i at 1 because the first line of every block is either (a) the opening line which must be empty or (b) the continuation of a line directly after a template substitution. Neither can be the start of a content line.
    3. Let i be 1.
    4. Repeat, while i < lineCount,
      1. Let line be lines[i].
      2. If line.[[LineEndsWithSubstitution]] is false and IsAllWhiteSpace(line.[[String]]) is true, then
        1. NOTE: Lines which contain only whitespace are emptied in the output. Their trailing newline is not removed, so the line is maintained.
        2. Set line.[[String]] to "".
      3. Set i to i + 1.

2.7 RemoveOpeningAndClosingLines ( blocks, len )

The abstract operation RemoveOpeningAndClosingLines takes arguments blocks (a List of Lists of Records with fields [[String]] (a String), [[Newline]] (a String), and [[LineEndsWithSubstitution]] (a Boolean)) and len (a non-negative integer). It performs the following steps when called:

  1. Assert: blocks contains at least 1 element, because we know the length of the template strings array is at least 1.
  2. Let firstBlock be blocks[0].
  3. Assert: firstBlock is not empty, because SplitTemplateIntoBlockLines guarantees there is at least 1 line per block.
  4. Let lineCount be the length of firstBlock.
  5. If lineCount = 1, then
    1. NOTE: The opening line is required to contain a trailing newline, and checking that there are at least 2 elements in lines ensures it. If it does not, either the opening line and the closing line are the same line, or the opening line contains a substitution.
    2. Throw a TypeError exception.
  6. Let openingLine be firstBlock[0].
  7. If openingLine.[[String]] is not empty, then
    1. NOTE: The opening line must not contain code units besides the trailing newline.
    2. Throw a TypeError exception.
  8. NOTE: Setting openingLine.[[Newline]] removes the opening line from the output.
  9. Set openingLine.[[Newline]] to "".
  10. Let lastBlock be blocks[len - 1].
  11. Assert: lastBlock is not empty, because SplitTemplateIntoBlockLines guarantees there is at least 1 line per block.
  12. Set lineCount to the length of lastBlock.
  13. If lineCount = 1, then
    1. NOTE: The closing line is required to be preceded by a newline, and checking that there are at least 2 elements in lines ensures it. If it does not, either the opening line and the closing line are the same line, or the closing line contains a substitution.
    2. Throw a TypeError exception.
  14. Let closingLine be lastBlock[lineCount - 1].
  15. If closingLine.[[String]] is not the empty String, then
    1. NOTE: The closing line may only contain whitespace. We've already performed EmptyWhiteSpaceLines, so if the line is not empty now, it contained some non-whitespace character.
    2. Throw a TypeError exception.
  16. Let preceding be lastBlock[lineCount - 2].
  17. NOTE: Setting closingLine.[[String]] and preceding.[[Newline]] removes the closing line from the output.
  18. Set closingLine.[[String]] to "".
  19. Set preceding.[[Newline]] to "".

2.8 DetermineCommonLeadingIndentation ( blocks )

The abstract operation DetermineCommonLeadingIndentation takes argument blocks (a List of Lists of Records with fields [[String]] (a String), [[Newline]] (a String), and [[LineEndsWithSubstitution]] (a Boolean)) and returns a String. It performs the following steps when called:

  1. Let common be empty.
  2. For each element lines of blocks, do
    1. Let lineCount be the length of lines.
    2. NOTE: We start i at 1 because because the first line of every block is either (a) the opening line which must be empty or (b) the continuation of a line directly after a template substitution. Neither can be the start of a content line.
    3. Let i be 1.
    4. Repeat, while i < lineCount,
      1. Let line be lines[i].
      2. NOTE: Lines which contain substitutions are considered when finding the common indentation. Lines which contain only whitespace have already been emptied.
      3. If line.[[LineEndsWithSubstitution]] is true or line.[[String]] is not the empty String, then
        1. Let leading be LeadingWhiteSpaceSubstring(line.[[String]]).
        2. If common is empty, then
          1. Set common to leading.
        3. Else,
          1. Set common to LongestMatchingLeadingSubstring(common, leading).
      4. Set i to i + 1.
  3. Assert: common is not empty, because SplitTemplateIntoBlockLines guarantees there is at least 1 line per block, and we know the length of the template strings array is at least 1.
  4. Return common.

2.9 LeadingWhiteSpaceSubstring ( str )

The abstract operation LeadingWhiteSpaceSubstring takes argument str (a String) and returns a String. It performs the following steps when called:

  1. Let len be the length of str.
  2. Let i be 0.
  3. Repeat, while i < len,
    1. Let c be the substring of str from i to i + 1.
    2. If c does not match WhiteSpace, then
      1. Return the substring of str from 0 to i.
    3. Set i to i + 1.
  4. Return str.

When determining whether a Unicode code point is in Unicode general category “Space_Separator” (“Zs”), code unit sequences are interpreted as UTF-16 encoded code point sequences as specified in 6.1.4.

2.10 IsAllWhiteSpace ( str )

The abstract operation IsAllWhiteSpace takes argument str (a String) and returns a Boolean. It performs the following steps when called:

  1. Let trimmed be ! TrimString(str, start).
  2. If trimmed is the empty String, return true.
  3. Return false.

2.11 LongestMatchingLeadingSubstring ( a, b )

The abstract operation LongestMatchingLeadingSubstring takes arguments a (a String) and b (a String) and returns a String. It performs the following steps when called:

  1. Let aLen be the length of a.
  2. Let bLen be the length of b.
  3. Let len be min(aLen, bLen).
  4. Let i be 0.
  5. Repeat, while i < len,
    1. Let aChar be the substring of a from i to i + 1.
    2. Let bChar be the substring of b from i to i + 1.
    3. If aChar is not bChar, then
      1. Return the substring of a from 0 to i.
    4. Set i to i + 1.
  6. Return the substring of a from 0 to len.

2.12 CookTemplateStringsArray ( template, substitutions, prep )

The abstract operation CookTemplateStringsArray takes arguments template (an ECMAScript language value), substitutions (a List of ECMAScript language values), and prep (cooked or raw) and returns either a normal completion containing a String or a throw completion.

Note

This abstract operation merges the behaviour of String.raw with the String.cooked proposal.

It performs the following steps when called:

  1. Let substitutionCount be the number of elements in substitutions.
  2. Let cooked be ? ToObject(template).
  3. If prep is cooked, let literals be cooked; else let literals be ? ToObject(? Get(cooked, "raw")).
  4. Let literalCount be ? LengthOfArrayLike(literals).
  5. If literalCount ≤ 0, return the empty String.
  6. Let R be the empty String.
  7. Let nextIndex be 0.
  8. Repeat,
    1. Let nextLiteralVal be ? Get(literals, ! ToString(𝔽(nextIndex))).
    2. Let nextLiteral be ? ToString(nextLiteralVal).
    3. Set R to the string-concatenation of R and nextLiteral.
    4. If nextIndex + 1 = literalCount, return R.
    5. If nextIndex < substitutionCount, then
      1. Let nextSubVal be substitutions[nextIndex].
      2. Let nextSub be ? ToString(nextSubVal).
      3. Set R to the string-concatenation of R and nextSub.
    6. Set nextIndex to nextIndex + 1.

3 Realms

Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.

A realm is represented in this specification as a Realm Record with the fields specified in Table 1:

Table 1: Realm Record Fields
Field Name Value Meaning
[[Intrinsics]] a Record whose field names are intrinsic keys and whose values are objects The intrinsic values used by code associated with this realm
[[GlobalObject]] an Object or undefined The global object for this realm
[[GlobalEnv]] a Global Environment Record The global environment for this realm
[[TemplateMap]] a List of Record { [[Site]]: Parse Node, [[Array]]: Object }

Template objects are canonicalized separately for each realm using its Realm Record's [[TemplateMap]]. Each [[Site]] value is a Parse Node that is a TemplateLiteral. The associated [[Array]] value is the corresponding template object that is passed to a tag function.

Note
Once a Parse Node becomes unreachable, the corresponding [[Array]] is also unreachable, and it would be unobservable if an implementation removed the pair from the [[TemplateMap]] list.
[[HostDefined]] anything (default value is undefined) Field reserved for use by hosts that need to associate additional information with a Realm Record.
[[DedentMap]] a List of Record { [[Raw]]: Object, [[Dedented]]: Object } The [[DedentMap]] ensures template tag functions wrapped with String.dedent see consistent object identity for a given input template. The [[Raw]] key weakly holds the value of a template object's raw property. The associated [[Dedented]] value is the result of performing dedenting on that raw value.

3.1 CreateRealm ( )

The abstract operation CreateRealm takes no arguments and returns a Realm Record. It performs the following steps when called:

  1. Let realmRec be a new Realm Record.
  2. Perform CreateIntrinsics(realmRec).
  3. Set realmRec.[[GlobalObject]] to undefined.
  4. Set realmRec.[[GlobalEnv]] to undefined.
  5. Set realmRec.[[TemplateMap]] to a new empty List.
  6. Set realmRec.[[DedentMap]] to a new empty List.
  7. Return realmRec.

4 Execution 9.10.3

At any time, if a set of objects S is not live, an ECMAScript implementation may perform the following steps atomically:

  1. For each element obj of S, do
    1. For each WeakRef ref such that ref.[[WeakRefTarget]] is obj, do
      1. Set ref.[[WeakRefTarget]] to empty.
    2. For each FinalizationRegistry fg such that fg.[[Cells]] contains a Record cell such that cell.[[WeakRefTarget]] is obj, do
      1. Set cell.[[WeakRefTarget]] to empty.
      2. Optionally, perform HostEnqueueFinalizationRegistryCleanupJob(fg).
    3. For each WeakMap map such that map.[[WeakMapData]] contains a Record r such that r.[[Key]] is obj, do
      1. Set r.[[Key]] to empty.
      2. Set r.[[Value]] to empty.
    4. For each WeakSet set such that set.[[WeakSetData]] contains obj, do
      1. Replace the element of set.[[WeakSetData]] whose value is obj with an element whose value is empty.
    5. Let realm be the current Realm Record.
    6. If realm.[[DedentMap]] contains a Record r such that r.[[Raw]] is obj, then
      1. Set r.[[Raw]] to empty.
      2. Set r.[[Dedented]] to empty.
Note 1

Together with the definition of liveness, this clause prescribes legal optimizations that an implementation may apply regarding WeakRefs.

It is possible to access an object without observing its identity. Optimizations such as dead variable elimination and scalar replacement on properties of non-escaping objects whose identity is not observed are allowed. These optimizations are thus allowed to observably empty WeakRefs that point to such objects.

On the other hand, if an object's identity is observable, and that object is in the [[WeakRefTarget]] internal slot of a WeakRef, optimizations such as rematerialization that observably empty the WeakRef are prohibited.

Because calling HostEnqueueFinalizationRegistryCleanupJob is optional, registered objects in a FinalizationRegistry do not necessarily hold that FinalizationRegistry live. Implementations may omit FinalizationRegistry callbacks for any reason, e.g., if the FinalizationRegistry itself becomes dead, or if the application is shutting down.

Note 2

Implementations are not obligated to empty WeakRefs for maximal sets of non-live objects.

If an implementation chooses a non-live set S in which to empty WeakRefs, it must empty WeakRefs for all objects in S simultaneously. In other words, an implementation must not empty a WeakRef pointing to an object obj without emptying out other WeakRefs that, if not emptied, could result in an execution that observes the Object value of obj.

A Copyright & Software License

Copyright Notice

© 2023 Justin Ridgewell

Software License

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.