Repository: tailwindlabs/tailwindcss

Problem statement:
Incorrect parsing of theme functions within calc operations without spaces
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of Tailwind CSS are you using?**

v3.3.1

**What build tool (or framework if it abstracts the build tool) are you using?**

[play.tailwindcss.com](https://play.tailwindcss.com/) or Webpack

**What version of Node.js are you using?**

N/A

**What browser are you using?**

Chrome

**What operating system are you using?**

macOS

**Reproduction URL**

https://play.tailwindcss.com/sQrIVhIaM7?file=css

**Describe your issue**

Usage of the `theme` function isn’t getting replaced by the corresponding value when the function is in a `calc` expression without spaces. For example:

```css
.test {
  /* Input: */
  inset: calc(-1 * (2*theme("spacing.4")));
  /* Expected: */
  inset: calc(-1 * (2*1rem));
  /* Actual: */
  inset: calc(-1 * (2*theme("spacing.4")));
}
```

---

I believe this might be the same issue as https://github.com/TrySound/postcss-value-parser/issues/86, but thought I’d report it here as I’m not 100% sure and it definitely affects Tailwind.

This happened for us because we use Sass alongside Tailwind, via Webpack. sass-loader apparently outputs Sass differently in Webpack production and development mode (https://github.com/webpack-contrib/sass-loader/issues/1129), with spaces removed in production.


Hints:

Patch:
diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js
--- a/src/lib/evaluateTailwindFunctions.js
+++ b/src/lib/evaluateTailwindFunctions.js
@@ -1,7 +1,7 @@
 import dlv from 'dlv'
 import didYouMean from 'didyoumean'
 import transformThemeValue from '../util/transformThemeValue'
-import parseValue from 'postcss-value-parser'
+import parseValue from '../value-parser/index'
 import { normalizeScreens } from '../util/normalizeScreens'
 import buildMediaQuery from '../util/buildMediaQuery'
 import { toPath } from '../util/toPath'
@@ -146,6 +146,9 @@ function resolveVNode(node, vNode, functions) {
 }
 
 function resolveFunctions(node, input, functions) {
+  let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`))
+  if (!hasAnyFn) return input
+
   return parseValue(input)
     .walk((vNode) => {
       resolveVNode(node, vNode, functions)
diff --git a/src/util/dataTypes.js b/src/util/dataTypes.js
--- a/src/util/dataTypes.js
+++ b/src/util/dataTypes.js
@@ -49,10 +49,22 @@ export function normalize(value, isRoot = true) {
     value = value.trim()
   }
 
-  // Add spaces around operators inside math functions like calc() that do not follow an operator
-  // or '('.
-  value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
+  value = normalizeMathOperatorSpacing(value)
+
+  return value
+}
+
+/**
+ * Add spaces around operators inside math functions
+ * like calc() that do not follow an operator or '('.
+ *
+ * @param {string} value
+ * @returns {string}
+ */
+function normalizeMathOperatorSpacing(value) {
+  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
     let vars = []
+
     return match
       .replace(/var\((--.+?)[,)]/g, (match, g1) => {
         vars.push(g1)
@@ -61,8 +73,6 @@ export function normalize(value, isRoot = true) {
       .replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ')
       .replace(placeholderRe, () => vars.shift())
   })
-
-  return value
 }
 
 export function url(value) {
diff --git a/src/value-parser/index.d.ts b/src/value-parser/index.d.ts
new file mode 100644
--- /dev/null
+++ b/src/value-parser/index.d.ts
@@ -0,0 +1,177 @@
+declare namespace postcssValueParser {
+  interface BaseNode {
+    /**
+     * The offset, inclusive, inside the CSS value at which the node starts.
+     */
+    sourceIndex: number
+
+    /**
+     * The offset, exclusive, inside the CSS value at which the node ends.
+     */
+    sourceEndIndex: number
+
+    /**
+     * The node's characteristic value
+     */
+    value: string
+  }
+
+  interface ClosableNode {
+    /**
+     * Whether the parsed CSS value ended before the node was properly closed
+     */
+    unclosed?: true
+  }
+
+  interface AdjacentAwareNode {
+    /**
+     * The token at the start of the node
+     */
+    before: string
+
+    /**
+     * The token at the end of the node
+     */
+    after: string
+  }
+
+  interface CommentNode extends BaseNode, ClosableNode {
+    type: 'comment'
+  }
+
+  interface DivNode extends BaseNode, AdjacentAwareNode {
+    type: 'div'
+  }
+
+  interface FunctionNode extends BaseNode, ClosableNode, AdjacentAwareNode {
+    type: 'function'
+
+    /**
+     * Nodes inside the function
+     */
+    nodes: Node[]
+  }
+
+  interface SpaceNode extends BaseNode {
+    type: 'space'
+  }
+
+  interface StringNode extends BaseNode, ClosableNode {
+    type: 'string'
+
+    /**
+     * The quote type delimiting the string
+     */
+    quote: '"' | "'"
+  }
+
+  interface UnicodeRangeNode extends BaseNode {
+    type: 'unicode-range'
+  }
+
+  interface WordNode extends BaseNode {
+    type: 'word'
+  }
+
+  /**
+   * Any node parsed from a CSS value
+   */
+  type Node =
+    | CommentNode
+    | DivNode
+    | FunctionNode
+    | SpaceNode
+    | StringNode
+    | UnicodeRangeNode
+    | WordNode
+
+  interface CustomStringifierCallback {
+    /**
+     * @param node The node to stringify
+     * @returns The serialized CSS representation of the node
+     */
+    (nodes: Node): string | undefined
+  }
+
+  interface WalkCallback {
+    /**
+     * @param node  The currently visited node
+     * @param index The index of the node in the series of parsed nodes
+     * @param nodes The series of parsed nodes
+     * @returns Returning `false` will prevent traversal of descendant nodes (only applies if `bubble` was set to `true` in the `walk()` call)
+     */
+    (node: Node, index: number, nodes: Node[]): void | boolean
+  }
+
+  /**
+   * A CSS dimension, decomposed into its numeric and unit parts
+   */
+  interface Dimension {
+    number: string
+    unit: string
+  }
+
+  /**
+   * A wrapper around a parsed CSS value that allows for inspecting and walking nodes
+   */
+  interface ParsedValue {
+    /**
+     * The series of parsed nodes
+     */
+    nodes: Node[]
+
+    /**
+     * Walk all parsed nodes, applying a callback
+     *
+     * @param callback A visitor callback that will be executed for each node
+     * @param bubble   When set to `true`, walking will be done inside-out instead of outside-in
+     */
+    walk(callback: WalkCallback, bubble?: boolean): this
+  }
+
+  interface ValueParser {
+    /**
+     * Decompose a CSS dimension into its numeric and unit part
+     *
+     * @param value The dimension to decompose
+     * @returns An object representing `number` and `unit` part of the dimension or `false` if the decomposing fails
+     */
+    unit(value: string): Dimension | false
+
+    /**
+     * Serialize a series of nodes into a CSS value
+     *
+     * @param nodes  The nodes to stringify
+     * @param custom A custom stringifier callback
+     * @returns The generated CSS value
+     */
+    stringify(nodes: Node | Node[], custom?: CustomStringifierCallback): string
+
+    /**
+     * Walk a series of nodes, applying a callback
+     *
+     * @param nodes    The nodes to walk
+     * @param callback A visitor callback that will be executed for each node
+     * @param bubble   When set to `true`, walking will be done inside-out instead of outside-in
+     */
+    walk(nodes: Node[], callback: WalkCallback, bubble?: boolean): void
+
+    /**
+     * Parse a CSS value into a series of nodes to operate on
+     *
+     * @param value The value to parse
+     */
+    new (value: string): ParsedValue
+
+    /**
+     * Parse a CSS value into a series of nodes to operate on
+     *
+     * @param value The value to parse
+     */
+    (value: string): ParsedValue
+  }
+}
+
+declare const postcssValueParser: postcssValueParser.ValueParser
+
+export = postcssValueParser
diff --git a/src/value-parser/index.js b/src/value-parser/index.js
new file mode 100644
--- /dev/null
+++ b/src/value-parser/index.js
@@ -0,0 +1,28 @@
+var parse = require('./parse')
+var walk = require('./walk')
+var stringify = require('./stringify')
+
+function ValueParser(value) {
+  if (this instanceof ValueParser) {
+    this.nodes = parse(value)
+    return this
+  }
+  return new ValueParser(value)
+}
+
+ValueParser.prototype.toString = function () {
+  return Array.isArray(this.nodes) ? stringify(this.nodes) : ''
+}
+
+ValueParser.prototype.walk = function (cb, bubble) {
+  walk(this.nodes, cb, bubble)
+  return this
+}
+
+ValueParser.unit = require('./unit')
+
+ValueParser.walk = walk
+
+ValueParser.stringify = stringify
+
+module.exports = ValueParser
diff --git a/src/value-parser/parse.js b/src/value-parser/parse.js
new file mode 100644
--- /dev/null
+++ b/src/value-parser/parse.js
@@ -0,0 +1,303 @@
+var openParentheses = '('.charCodeAt(0)
+var closeParentheses = ')'.charCodeAt(0)
+var singleQuote = "'".charCodeAt(0)
+var doubleQuote = '"'.charCodeAt(0)
+var backslash = '\\'.charCodeAt(0)
+var slash = '/'.charCodeAt(0)
+var comma = ','.charCodeAt(0)
+var colon = ':'.charCodeAt(0)
+var star = '*'.charCodeAt(0)
+var uLower = 'u'.charCodeAt(0)
+var uUpper = 'U'.charCodeAt(0)
+var plus = '+'.charCodeAt(0)
+var isUnicodeRange = /^[a-f0-9?-]+$/i
+
+module.exports = function (input) {
+  var tokens = []
+  var value = input
+
+  var next, quote, prev, token, escape, escapePos, whitespacePos, parenthesesOpenPos
+  var pos = 0
+  var code = value.charCodeAt(pos)
+  var max = value.length
+  var stack = [{ nodes: tokens }]
+  var balanced = 0
+  var parent
+
+  var name = ''
+  var before = ''
+  var after = ''
+
+  while (pos < max) {
+    // Whitespaces
+    if (code <= 32) {
+      next = pos
+      do {
+        next += 1
+        code = value.charCodeAt(next)
+      } while (code <= 32)
+      token = value.slice(pos, next)
+
+      prev = tokens[tokens.length - 1]
+      if (code === closeParentheses && balanced) {
+        after = token
+      } else if (prev && prev.type === 'div') {
+        prev.after = token
+        prev.sourceEndIndex += token.length
+      } else if (
+        code === comma ||
+        code === colon ||
+        (code === slash &&
+          value.charCodeAt(next + 1) !== star &&
+          (!parent || (parent && parent.type === 'function' && false)))
+      ) {
+        before = token
+      } else {
+        tokens.push({
+          type: 'space',
+          sourceIndex: pos,
+          sourceEndIndex: next,
+          value: token,
+        })
+      }
+
+      pos = next
+
+      // Quotes
+    } else if (code === singleQuote || code === doubleQuote) {
+      next = pos
+      quote = code === singleQuote ? "'" : '"'
+      token = {
+        type: 'string',
+        sourceIndex: pos,
+        quote: quote,
+      }
+      do {
+        escape = false
+        next = value.indexOf(quote, next + 1)
+        if (~next) {
+          escapePos = next
+          while (value.charCodeAt(escapePos - 1) === backslash) {
+            escapePos -= 1
+            escape = !escape
+          }
+        } else {
+          value += quote
+          next = value.length - 1
+          token.unclosed = true
+        }
+      } while (escape)
+      token.value = value.slice(pos + 1, next)
+      token.sourceEndIndex = token.unclosed ? next : next + 1
+      tokens.push(token)
+      pos = next + 1
+      code = value.charCodeAt(pos)
+
+      // Comments
+    } else if (code === slash && value.charCodeAt(pos + 1) === star) {
+      next = value.indexOf('*/', pos)
+
+      token = {
+        type: 'comment',
+        sourceIndex: pos,
+        sourceEndIndex: next + 2,
+      }
+
+      if (next === -1) {
+        token.unclosed = true
+        next = value.length
+        token.sourceEndIndex = next
+      }
+
+      token.value = value.slice(pos + 2, next)
+      tokens.push(token)
+
+      pos = next + 2
+      code = value.charCodeAt(pos)
+
+      // Operation within calc
+    } else if ((code === slash || code === star) && parent && parent.type === 'function' && true) {
+      token = value[pos]
+      tokens.push({
+        type: 'word',
+        sourceIndex: pos - before.length,
+        sourceEndIndex: pos + token.length,
+        value: token,
+      })
+      pos += 1
+      code = value.charCodeAt(pos)
+
+      // Dividers
+    } else if (code === slash || code === comma || code === colon) {
+      token = value[pos]
+
+      tokens.push({
+        type: 'div',
+        sourceIndex: pos - before.length,
+        sourceEndIndex: pos + token.length,
+        value: token,
+        before: before,
+        after: '',
+      })
+      before = ''
+
+      pos += 1
+      code = value.charCodeAt(pos)
+
+      // Open parentheses
+    } else if (openParentheses === code) {
+      // Whitespaces after open parentheses
+      next = pos
+      do {
+        next += 1
+        code = value.charCodeAt(next)
+      } while (code <= 32)
+      parenthesesOpenPos = pos
+      token = {
+        type: 'function',
+        sourceIndex: pos - name.length,
+        value: name,
+        before: value.slice(parenthesesOpenPos + 1, next),
+      }
+      pos = next
+
+      if (name === 'url' && code !== singleQuote && code !== doubleQuote) {
+        next -= 1
+        do {
+          escape = false
+          next = value.indexOf(')', next + 1)
+          if (~next) {
+            escapePos = next
+            while (value.charCodeAt(escapePos - 1) === backslash) {
+              escapePos -= 1
+              escape = !escape
+            }
+          } else {
+            value += ')'
+            next = value.length - 1
+            token.unclosed = true
+          }
+        } while (escape)
+        // Whitespaces before closed
+        whitespacePos = next
+        do {
+          whitespacePos -= 1
+          code = value.charCodeAt(whitespacePos)
+        } while (code <= 32)
+        if (parenthesesOpenPos < whitespacePos) {
+          if (pos !== whitespacePos + 1) {
+            token.nodes = [
+              {
+                type: 'word',
+                sourceIndex: pos,
+                sourceEndIndex: whitespacePos + 1,
+                value: value.slice(pos, whitespacePos + 1),
+              },
+            ]
+          } else {
+            token.nodes = []
+          }
+          if (token.unclosed && whitespacePos + 1 !== next) {
+            token.after = ''
+            token.nodes.push({
+              type: 'space',
+              sourceIndex: whitespacePos + 1,
+              sourceEndIndex: next,
+              value: value.slice(whitespacePos + 1, next),
+            })
+          } else {
+            token.after = value.slice(whitespacePos + 1, next)
+            token.sourceEndIndex = next
+          }
+        } else {
+          token.after = ''
+          token.nodes = []
+        }
+        pos = next + 1
+        token.sourceEndIndex = token.unclosed ? next : pos
+        code = value.charCodeAt(pos)
+        tokens.push(token)
+      } else {
+        balanced += 1
+        token.after = ''
+        token.sourceEndIndex = pos + 1
+        tokens.push(token)
+        stack.push(token)
+        tokens = token.nodes = []
+        parent = token
+      }
+      name = ''
+
+      // Close parentheses
+    } else if (closeParentheses === code && balanced) {
+      pos += 1
+      code = value.charCodeAt(pos)
+
+      parent.after = after
+      parent.sourceEndIndex += after.length
+      after = ''
+      balanced -= 1
+      stack[stack.length - 1].sourceEndIndex = pos
+      stack.pop()
+      parent = stack[balanced]
+      tokens = parent.nodes
+
+      // Words
+    } else {
+      next = pos
+      do {
+        if (code === backslash) {
+          next += 1
+        }
+        next += 1
+        code = value.charCodeAt(next)
+      } while (
+        next < max &&
+        !(
+          code <= 32 ||
+          code === singleQuote ||
+          code === doubleQuote ||
+          code === comma ||
+          code === colon ||
+          code === slash ||
+          code === openParentheses ||
+          (code === star && parent && parent.type === 'function' && true) ||
+          (code === slash && parent.type === 'function' && true) ||
+          (code === closeParentheses && balanced)
+        )
+      )
+      token = value.slice(pos, next)
+
+      if (openParentheses === code) {
+        name = token
+      } else if (
+        (uLower === token.charCodeAt(0) || uUpper === token.charCodeAt(0)) &&
+        plus === token.charCodeAt(1) &&
+        isUnicodeRange.test(token.slice(2))
+      ) {
+        tokens.push({
+          type: 'unicode-range',
+          sourceIndex: pos,
+          sourceEndIndex: next,
+          value: token,
+        })
+      } else {
+        tokens.push({
+          type: 'word',
+          sourceIndex: pos,
+          sourceEndIndex: next,
+          value: token,
+        })
+      }
+
+      pos = next
+    }
+  }
+
+  for (pos = stack.length - 1; pos; pos -= 1) {
+    stack[pos].unclosed = true
+    stack[pos].sourceEndIndex = value.length
+  }
+
+  return stack[0].nodes
+}
diff --git a/src/value-parser/stringify.js b/src/value-parser/stringify.js
new file mode 100644
--- /dev/null
+++ b/src/value-parser/stringify.js
@@ -0,0 +1,41 @@
+function stringifyNode(node, custom) {
+  var type = node.type
+  var value = node.value
+  var buf
+  var customResult
+
+  if (custom && (customResult = custom(node)) !== undefined) {
+    return customResult
+  } else if (type === 'word' || type === 'space') {
+    return value
+  } else if (type === 'string') {
+    buf = node.quote || ''
+    return buf + value + (node.unclosed ? '' : buf)
+  } else if (type === 'comment') {
+    return '/*' + value + (node.unclosed ? '' : '*/')
+  } else if (type === 'div') {
+    return (node.before || '') + value + (node.after || '')
+  } else if (Array.isArray(node.nodes)) {
+    buf = stringify(node.nodes, custom)
+    if (type !== 'function') {
+      return buf
+    }
+    return value + '(' + (node.before || '') + buf + (node.after || '') + (node.unclosed ? '' : ')')
+  }
+  return value
+}
+
+function stringify(nodes, custom) {
+  var result, i
+
+  if (Array.isArray(nodes)) {
+    result = ''
+    for (i = nodes.length - 1; ~i; i -= 1) {
+      result = stringifyNode(nodes[i], custom) + result
+    }
+    return result
+  }
+  return stringifyNode(nodes, custom)
+}
+
+module.exports = stringify
diff --git a/src/value-parser/unit.js b/src/value-parser/unit.js
new file mode 100644
--- /dev/null
+++ b/src/value-parser/unit.js
@@ -0,0 +1,118 @@
+var minus = '-'.charCodeAt(0)
+var plus = '+'.charCodeAt(0)
+var dot = '.'.charCodeAt(0)
+var exp = 'e'.charCodeAt(0)
+var EXP = 'E'.charCodeAt(0)
+
+// Check if three code points would start a number
+// https://www.w3.org/TR/css-syntax-3/#starts-with-a-number
+function likeNumber(value) {
+  var code = value.charCodeAt(0)
+  var nextCode
+
+  if (code === plus || code === minus) {
+    nextCode = value.charCodeAt(1)
+
+    if (nextCode >= 48 && nextCode <= 57) {
+      return true
+    }
+
+    var nextNextCode = value.charCodeAt(2)
+
+    if (nextCode === dot && nextNextCode >= 48 && nextNextCode <= 57) {
+      return true
+    }
+
+    return false
+  }
+
+  if (code === dot) {
+    nextCode = value.charCodeAt(1)
+
+    if (nextCode >= 48 && nextCode <= 57) {
+      return true
+    }
+
+    return false
+  }
+
+  if (code >= 48 && code <= 57) {
+    return true
+  }
+
+  return false
+}
+
+// Consume a number
+// https://www.w3.org/TR/css-syntax-3/#consume-number
+module.exports = function (value) {
+  var pos = 0
+  var length = value.length
+  var code
+  var nextCode
+  var nextNextCode
+
+  if (length === 0 || !likeNumber(value)) {
+    return false
+  }
+
+  code = value.charCodeAt(pos)
+
+  if (code === plus || code === minus) {
+    pos++
+  }
+
+  while (pos < length) {
+    code = value.charCodeAt(pos)
+
+    if (code < 48 || code > 57) {
+      break
+    }
+
+    pos += 1
+  }
+
+  code = value.charCodeAt(pos)
+  nextCode = value.charCodeAt(pos + 1)
+
+  if (code === dot && nextCode >= 48 && nextCode <= 57) {
+    pos += 2
+
+    while (pos < length) {
+      code = value.charCodeAt(pos)
+
+      if (code < 48 || code > 57) {
+        break
+      }
+
+      pos += 1
+    }
+  }
+
+  code = value.charCodeAt(pos)
+  nextCode = value.charCodeAt(pos + 1)
+  nextNextCode = value.charCodeAt(pos + 2)
+
+  if (
+    (code === exp || code === EXP) &&
+    ((nextCode >= 48 && nextCode <= 57) ||
+      ((nextCode === plus || nextCode === minus) && nextNextCode >= 48 && nextNextCode <= 57))
+  ) {
+    pos += nextCode === plus || nextCode === minus ? 3 : 2
+
+    while (pos < length) {
+      code = value.charCodeAt(pos)
+
+      if (code < 48 || code > 57) {
+        break
+      }
+
+      pos += 1
+    }
+  }
+
+  return {
+    number: value.slice(0, pos),
+    unit: value.slice(pos),
+  }
+}
diff --git a/src/value-parser/walk.js b/src/value-parser/walk.js
new file mode 100644
--- /dev/null
+++ b/src/value-parser/walk.js
@@ -0,0 +1,18 @@
+module.exports = function walk(nodes, cb, bubble) {
+  var i, max, node, result
+
+  for (i = 0, max = nodes.length; i < max; i += 1) {
+    node = nodes[i]
+    if (!bubble) {
+      result = cb(node, i, nodes)
+    }
+
+    if (result !== false && node.type === 'function' && Array.isArray(node.nodes)) {
+      walk(node.nodes, cb, bubble)
+    }
+
+    if (bubble) {
+      cb(node, i, nodes)
+    }
+  }
+}


Test patch:
diff --git a/tests/evaluateTailwindFunctions.test.js b/tests/evaluateTailwindFunctions.test.js
--- a/tests/evaluateTailwindFunctions.test.js
+++ b/tests/evaluateTailwindFunctions.test.js
@@ -1383,5 +1383,36 @@ crosscheck(({ stable, oxide }) => {
       // 4. But we've not received any further logs about it
       expect().toHaveBeenWarnedWith(['invalid-theme-key-in-class'])
     })
+
+    test('it works mayhaps', async () => {
+      let input = css`
+        .test {
+          /* prettier-ignore */
+          inset: calc(-1 * (2*theme("spacing.4")));
+          /* prettier-ignore */
+          padding: calc(-1 * (2* theme("spacing.4")));
+        }
+      `
+
+      let output = css`
+        .test {
+          /* prettier-ignore */
+          inset: calc(-1 * (2*1rem));
+          /* prettier-ignore */
+          padding: calc(-1 * (2* 1rem));
+        }
+      `
+
+      return run(input, {
+        theme: {
+          spacing: {
+            4: '1rem',
+          },
+        },
+      }).then((result) => {
+        expect(result.css).toMatchCss(output)
+        expect(result.warnings().length).toBe(0)
+      })
+    })
   })
 })
diff --git a/tests/source-maps.test.js b/tests/source-maps.test.js
--- a/tests/source-maps.test.js
+++ b/tests/source-maps.test.js
@@ -260,145 +260,143 @@ crosscheck(({ stable, oxide }) => {
       '2:6-20 -> 304:2-12',
       '2:20 -> 305:0',
       '2:6 -> 307:0',
-      '2:20 -> 309:1',
-      '2:6 -> 310:0',
-      '2:6-20 -> 311:2-12',
-      '2:20 -> 312:0',
-      '2:6 -> 314:0',
-      '2:20 -> 316:1',
-      '2:6 -> 318:0',
-      '2:6-20 -> 319:2-18',
-      '2:20 -> 320:0',
-      '2:6 -> 322:0',
-      '2:20 -> 325:1',
-      '2:6 -> 327:0',
-      '2:6-20 -> 329:2-20',
-      '2:6-20 -> 330:2-24',
-      '2:20 -> 331:0',
-      '2:6 -> 333:0',
-      '2:20 -> 335:1',
-      '2:6 -> 337:0',
-      '2:6-20 -> 339:2-17',
-      '2:20 -> 340:0',
+      '2:6-20 -> 308:2-12',
+      '2:20 -> 309:0',
+      '2:6 -> 311:0',
+      '2:20 -> 313:1',
+      '2:6 -> 315:0',
+      '2:6-20 -> 316:2-18',
+      '2:20 -> 317:0',
+      '2:6 -> 319:0',
+      '2:20 -> 322:1',
+      '2:6 -> 324:0',
+      '2:6-20 -> 326:2-20',
+      '2:6-20 -> 327:2-24',
+      '2:20 -> 328:0',
+      '2:6 -> 330:0',
+      '2:20 -> 332:1',
+      '2:6 -> 334:0',
+      '2:6-20 -> 336:2-17',
+      '2:20 -> 337:0',
+      '2:6 -> 339:0',
+      '2:20 -> 341:1',
       '2:6 -> 342:0',
-      '2:20 -> 344:1',
-      '2:6 -> 345:0',
-      '2:6-20 -> 346:2-17',
-      '2:20 -> 347:0',
-      '2:6 -> 349:0',
-      '2:20 -> 353:1',
-      '2:6 -> 355:0',
-      '2:6-20 -> 363:2-24',
-      '2:6-20 -> 364:2-32',
-      '2:20 -> 365:0',
-      '2:6 -> 367:0',
-      '2:20 -> 369:1',
-      '2:6 -> 371:0',
-      '2:6-20 -> 373:2-17',
-      '2:6-20 -> 374:2-14',
-      '2:20 -> 375:0',
-      '2:6-20 -> 377:0-72',
-      '2:6 -> 378:0',
-      '2:6-20 -> 379:2-15',
-      '2:20 -> 380:0',
-      '2:6 -> 382:0',
-      '2:6-20 -> 383:2-26',
-      '2:6-20 -> 384:2-26',
-      '2:6-20 -> 385:2-21',
-      '2:6-20 -> 386:2-21',
-      '2:6-20 -> 387:2-16',
-      '2:6-20 -> 388:2-16',
-      '2:6-20 -> 389:2-16',
-      '2:6-20 -> 390:2-17',
-      '2:6-20 -> 391:2-17',
-      '2:6-20 -> 392:2-15',
-      '2:6-20 -> 393:2-15',
-      '2:6-20 -> 394:2-20',
-      '2:6-20 -> 395:2-40',
-      '2:6-20 -> 396:2-32',
-      '2:6-20 -> 397:2-31',
-      '2:6-20 -> 398:2-30',
-      '2:6-20 -> 399:2-17',
-      '2:6-20 -> 400:2-22',
-      '2:6-20 -> 401:2-24',
-      '2:6-20 -> 402:2-25',
-      '2:6-20 -> 403:2-26',
-      '2:6-20 -> 404:2-20',
-      '2:6-20 -> 405:2-29',
-      '2:6-20 -> 406:2-30',
-      '2:6-20 -> 407:2-40',
-      '2:6-20 -> 408:2-36',
-      '2:6-20 -> 409:2-29',
-      '2:6-20 -> 410:2-24',
-      '2:6-20 -> 411:2-32',
-      '2:6-20 -> 412:2-14',
+      '2:6-20 -> 343:2-17',
+      '2:20 -> 344:0',
+      '2:6 -> 346:0',
+      '2:20 -> 350:1',
+      '2:6 -> 352:0',
+      '2:6-20 -> 360:2-24',
+      '2:6-20 -> 361:2-32',
+      '2:20 -> 362:0',
+      '2:6 -> 364:0',
+      '2:20 -> 366:1',
+      '2:6 -> 368:0',
+      '2:6-20 -> 370:2-17',
+      '2:6-20 -> 371:2-14',
+      '2:20 -> 372:0',
+      '2:6-20 -> 374:0-72',
+      '2:6 -> 375:0',
+      '2:6-20 -> 376:2-15',
+      '2:20 -> 377:0',
+      '2:6 -> 379:0',
+      '2:6-20 -> 380:2-26',
+      '2:6-20 -> 381:2-26',
+      '2:6-20 -> 382:2-21',
+      '2:6-20 -> 383:2-21',
+      '2:6-20 -> 384:2-16',
+      '2:6-20 -> 385:2-16',
+      '2:6-20 -> 386:2-16',
+      '2:6-20 -> 387:2-17',
+      '2:6-20 -> 388:2-17',
+      '2:6-20 -> 389:2-15',
+      '2:6-20 -> 390:2-15',
+      '2:6-20 -> 391:2-20',
+      '2:6-20 -> 392:2-40',
+      '2:6-20 -> 393:2-32',
+      '2:6-20 -> 394:2-31',
+      '2:6-20 -> 395:2-30',
+      '2:6-20 -> 396:2-17',
+      '2:6-20 -> 397:2-22',
+      '2:6-20 -> 398:2-24',
+      '2:6-20 -> 399:2-25',
+      '2:6-20 -> 400:2-26',
+      '2:6-20 -> 401:2-20',
+      '2:6-20 -> 402:2-29',
+      '2:6-20 -> 403:2-30',
+      '2:6-20 -> 404:2-40',
+      '2:6-20 -> 405:2-36',
+      '2:6-20 -> 406:2-29',
+      '2:6-20 -> 407:2-24',
+      '2:6-20 -> 408:2-32',
+      '2:6-20 -> 409:2-14',
+      '2:6-20 -> 410:2-20',
+      '2:6-20 -> 411:2-18',
+      '2:6-20 -> 412:2-19',
       '2:6-20 -> 413:2-20',
-      '2:6-20 -> 414:2-18',
-      '2:6-20 -> 415:2-19',
-      '2:6-20 -> 416:2-20',
-      '2:6-20 -> 417:2-16',
-      '2:6-20 -> 418:2-18',
-      '2:6-20 -> 419:2-15',
-      '2:6-20 -> 420:2-21',
-      '2:6-20 -> 421:2-23',
+      '2:6-20 -> 414:2-16',
+      '2:6-20 -> 415:2-18',
+      '2:6-20 -> 416:2-15',
+      '2:6-20 -> 417:2-21',
+      '2:6-20 -> 418:2-23',
+      '2:6-20 -> 419:2-29',
+      '2:6-20 -> 420:2-27',
+      '2:6-20 -> 421:2-28',
       '2:6-20 -> 422:2-29',
-      '2:6-20 -> 423:2-27',
-      '2:6-20 -> 424:2-28',
-      '2:6-20 -> 425:2-29',
-      '2:6-20 -> 426:2-25',
-      '2:6-20 -> 427:2-26',
-      '2:6-20 -> 428:2-27',
-      '2:6 -> 429:2',
-      '2:20 -> 430:0',
-      '2:6 -> 432:0',
-      '2:6-20 -> 433:2-26',
-      '2:6-20 -> 434:2-26',
-      '2:6-20 -> 435:2-21',
-      '2:6-20 -> 436:2-21',
-      '2:6-20 -> 437:2-16',
-      '2:6-20 -> 438:2-16',
-      '2:6-20 -> 439:2-16',
-      '2:6-20 -> 440:2-17',
-      '2:6-20 -> 441:2-17',
-      '2:6-20 -> 442:2-15',
-      '2:6-20 -> 443:2-15',
-      '2:6-20 -> 444:2-20',
-      '2:6-20 -> 445:2-40',
-      '2:6-20 -> 446:2-32',
-      '2:6-20 -> 447:2-31',
-      '2:6-20 -> 448:2-30',
-      '2:6-20 -> 449:2-17',
-      '2:6-20 -> 450:2-22',
-      '2:6-20 -> 451:2-24',
-      '2:6-20 -> 452:2-25',
-      '2:6-20 -> 453:2-26',
-      '2:6-20 -> 454:2-20',
-      '2:6-20 -> 455:2-29',
-      '2:6-20 -> 456:2-30',
-      '2:6-20 -> 457:2-40',
-      '2:6-20 -> 458:2-36',
-      '2:6-20 -> 459:2-29',
-      '2:6-20 -> 460:2-24',
-      '2:6-20 -> 461:2-32',
-      '2:6-20 -> 462:2-14',
+      '2:6-20 -> 423:2-25',
+      '2:6-20 -> 424:2-26',
+      '2:6-20 -> 425:2-27',
+      '2:6 -> 426:2',
+      '2:20 -> 427:0',
+      '2:6 -> 429:0',
+      '2:6-20 -> 430:2-26',
+      '2:6-20 -> 431:2-26',
+      '2:6-20 -> 432:2-21',
+      '2:6-20 -> 433:2-21',
+      '2:6-20 -> 434:2-16',
+      '2:6-20 -> 435:2-16',
+      '2:6-20 -> 436:2-16',
+      '2:6-20 -> 437:2-17',
+      '2:6-20 -> 438:2-17',
+      '2:6-20 -> 439:2-15',
+      '2:6-20 -> 440:2-15',
+      '2:6-20 -> 441:2-20',
+      '2:6-20 -> 442:2-40',
+      '2:6-20 -> 443:2-32',
+      '2:6-20 -> 444:2-31',
+      '2:6-20 -> 445:2-30',
+      '2:6-20 -> 446:2-17',
+      '2:6-20 -> 447:2-22',
+      '2:6-20 -> 448:2-24',
+      '2:6-20 -> 449:2-25',
+      '2:6-20 -> 450:2-26',
+      '2:6-20 -> 451:2-20',
+      '2:6-20 -> 452:2-29',
+      '2:6-20 -> 453:2-30',
+      '2:6-20 -> 454:2-40',
+      '2:6-20 -> 455:2-36',
+      '2:6-20 -> 456:2-29',
+      '2:6-20 -> 457:2-24',
+      '2:6-20 -> 458:2-32',
+      '2:6-20 -> 459:2-14',
+      '2:6-20 -> 460:2-20',
+      '2:6-20 -> 461:2-18',
+      '2:6-20 -> 462:2-19',
       '2:6-20 -> 463:2-20',
-      '2:6-20 -> 464:2-18',
-      '2:6-20 -> 465:2-19',
-      '2:6-20 -> 466:2-20',
-      '2:6-20 -> 467:2-16',
-      '2:6-20 -> 468:2-18',
-      '2:6-20 -> 469:2-15',
-      '2:6-20 -> 470:2-21',
-      '2:6-20 -> 471:2-23',
+      '2:6-20 -> 464:2-16',
+      '2:6-20 -> 465:2-18',
+      '2:6-20 -> 466:2-15',
+      '2:6-20 -> 467:2-21',
+      '2:6-20 -> 468:2-23',
+      '2:6-20 -> 469:2-29',
+      '2:6-20 -> 470:2-27',
+      '2:6-20 -> 471:2-28',
       '2:6-20 -> 472:2-29',
-      '2:6-20 -> 473:2-27',
-      '2:6-20 -> 474:2-28',
-      '2:6-20 -> 475:2-29',
-      '2:6-20 -> 476:2-25',
-      '2:6-20 -> 477:2-26',
-      '2:6-20 -> 478:2-27',
-      '2:6 -> 479:2',
-      '2:20 -> 480:0',
+      '2:6-20 -> 473:2-25',
+      '2:6-20 -> 474:2-26',
+      '2:6-20 -> 475:2-27',
+      '2:6 -> 476:2',
+      '2:20 -> 477:0',
     ])
   })