shaderkit 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/minifier.ts CHANGED
@@ -9,111 +9,78 @@ export interface MinifyOptions {
9
9
  mangleMap: Map<string, string>
10
10
  /** Whether to rename external variables such as uniforms or varyings. Default is `false`. */
11
11
  mangleExternals: boolean
12
- /** Whether to rename properties in structs or uniform buffers. Default is `false`. */
13
- mangleProperties: boolean
14
12
  }
15
13
 
14
+ const isWord = RegExp.prototype.test.bind(/^\w/)
15
+ const isName = RegExp.prototype.test.bind(/^[A-Za-z]/)
16
+ const isStorage = RegExp.prototype.test.bind(/^(uniform|in|out|attribute|varying|,)$/)
17
+
16
18
  /**
17
- * Minifies a string of GLSL code.
19
+ * Minifies a string of GLSL or WGSL code.
18
20
  */
19
21
  export function minify(
20
22
  code: string,
21
- {
22
- mangle = false,
23
- mangleMap = new Map(),
24
- mangleExternals = false,
25
- mangleProperties = false,
26
- }: Partial<MinifyOptions> = {},
23
+ { mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
27
24
  ): string {
25
+ // Escape newlines after directives, skip comments
26
+ code = code.replace(/(^\s*#[^\\]*?)(\n|\/[\/\*])/gm, '$1\\$2')
27
+
28
28
  const exclude = new Set<string>(mangleMap.values())
29
- const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'comment')
29
+ const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
30
30
 
31
31
  let mangleIndex: number = 0
32
32
  let blockIndex: number | null = null
33
- let prefix: string | null = null
34
33
  let minified: string = ''
35
34
  for (let i = 0; i < tokens.length; i++) {
36
35
  const token = tokens[i]
37
- if (/keyword|identifier/.test(token.type)) {
38
- if (/keyword|identifier/.test(tokens[i - 1]?.type)) minified += ' '
39
36
 
40
- // Resolve nested keys from members or accessors
41
- let key = token.value
42
- if (prefix) {
43
- key = `${prefix}.${token.value}`
44
- } else if (tokens[i - 1]?.value === '.' && tokens[i - 2]?.type !== 'symbol') {
45
- key = `${tokens[i - 2]?.value}.${token.value}`
46
- }
37
+ // Pad alphanumeric tokens
38
+ if (isWord(token.value) && isWord(tokens[i - 1]?.value)) minified += ' '
39
+
40
+ // Mark enter/leave block-scope
41
+ if (token.value === '{' && isName(tokens[i - 1]?.value)) blockIndex = i - 1
42
+ else if (token.value === '}') blockIndex = null
47
43
 
48
- // Mangle declarations and their references
49
- let renamed = mangleMap.get(key)
44
+ // Mangle declarations and their references
45
+ if (isName(token.value)) {
46
+ let renamed = mangleMap.get(token.value)
50
47
  if (
51
48
  // no-op
52
49
  !renamed &&
50
+ // Skip struct properties
51
+ blockIndex == null &&
53
52
  // Filter variable names
54
- key !== 'main' &&
53
+ token.value !== 'main' &&
55
54
  (typeof mangle === 'boolean' ? mangle : mangle(token, i, tokens)) &&
56
55
  // Is declaration, reference, namespace, or comma-separated list
57
56
  token.type === 'identifier' &&
58
- (/keyword|identifier/.test(tokens[i - 1]?.type) || /}|,/.test(tokens[i - 1]?.value))
57
+ (isName(tokens[i - 1]?.value) || /}|,/.test(tokens[i - 1]?.value) || tokens[i + 1]?.value === ':') &&
58
+ // Skip shader externals when disabled
59
+ (mangleExternals || (!isStorage(tokens[i - 1]?.value) && !isStorage(tokens[i - 2]?.value)))
59
60
  ) {
60
- if (
61
- // Skip struct properties when specified
62
- (!prefix || mangleProperties) &&
63
- // Skip shader externals when disabled
64
- (blockIndex != null
65
- ? // Struct member
66
- (mangleExternals && mangleProperties) ||
67
- !/(struct|uniform|in|out|attribute|varying)/.test(tokens[blockIndex - 1]?.value)
68
- : // Struct header, fully specified uniform, or comma-separated list
69
- mangleExternals ||
70
- (!/(uniform|in|out|attribute|varying|,)/.test(tokens[i - 1]?.value) &&
71
- !/(uniform|in|out|attribute|varying)/.test(tokens[i - 2]?.value)))
72
- ) {
73
- while (!renamed || exclude.has(renamed)) {
74
- renamed = ''
75
- mangleIndex++
61
+ while (!renamed || exclude.has(renamed)) {
62
+ renamed = ''
63
+ mangleIndex++
76
64
 
77
- let j = mangleIndex
78
- while (j > 0) {
79
- renamed = String.fromCharCode(97 + ((j % 26) - 1)) + renamed
80
- j = Math.floor(j / 26)
81
- }
65
+ let j = mangleIndex
66
+ while (j > 0) {
67
+ renamed = String.fromCharCode(97 + ((j % 26) - 1)) + renamed
68
+ j = Math.floor(j / 26)
82
69
  }
83
-
84
- mangleMap.set(key, renamed)
85
70
  }
86
71
 
87
- // Start or stop capturing namespaces, prefer UBO suffix if specified.
88
- // If UBOs don't specify a suffix, their inner declarations are global
89
- if (tokens[i + 1]?.value === '{') {
90
- let j = i
91
- while (tokens[j].value !== '}') j++
92
- const suffix = tokens[j + 1].value
93
- if (suffix !== ';') prefix = suffix
94
- blockIndex = i
95
- } else if (tokens[i + 2]?.value === '}') {
96
- prefix = null
97
- blockIndex = null
98
- }
72
+ mangleMap.set(token.value, renamed)
99
73
  }
100
74
 
101
75
  minified += renamed ?? token.value
102
- } else if (token.value === '#') {
103
- // Don't pad consecutive directives
104
- if (tokens[i - 1]?.value !== '\\') minified += '\n'
105
-
106
- // Join preprocessor directives
107
- while (tokens[i].value !== '\\') {
108
- if ((tokens[i].type !== 'symbol' || tokens[i - 2]?.value === '#') && tokens[i - 1]?.type !== 'symbol')
109
- minified += ' '
110
- minified += tokens[i].value
111
- i++
112
- }
113
76
 
114
- minified += '\n'
77
+ // three.js has a white-space sensitive RegExp
78
+ // https://github.com/mrdoob/three.js/blob/dev/src/renderers/webgl/WebGLProgram.js#L206
79
+ if (token.value === 'include') minified += ' '
115
80
  } else {
116
- minified += token.value
81
+ if (token.value === '#' && tokens[i - 1]?.value !== '\\') minified += '\n#'
82
+ else if (token.value === '\\') minified += '\n'
83
+ else minified += token.value
117
84
  }
118
85
  }
119
86
 
package/src/tokenizer.ts CHANGED
@@ -1,64 +1,73 @@
1
- import { GLSL_SYMBOLS, GLSL_KEYWORDS } from './constants'
1
+ import { SYMBOLS, WGSL_KEYWORDS, GLSL_KEYWORDS } from './constants'
2
2
 
3
- export type TokenType = 'comment' | 'symbol' | 'bool' | 'float' | 'int' | 'uint' | 'identifier' | 'keyword'
3
+ export type TokenType = 'whitespace' | 'comment' | 'symbol' | 'bool' | 'float' | 'int' | 'identifier' | 'keyword'
4
4
 
5
5
  export interface Token<T = TokenType, V = string> {
6
6
  type: T
7
7
  value: V
8
8
  }
9
9
 
10
+ // Checks for WGSL-specific `fn foo(`, `var bar =`, and `let baz =`
11
+ const isWGSL = RegExp.prototype.test.bind(/\bfn\s+\w+\s*\(|\b(var|let)\b/)
12
+
13
+ const isFloat = RegExp.prototype.test.bind(/\.|[eEpP][-+]?\d|[fFhH]$/)
14
+ const isBool = RegExp.prototype.test.bind(/^(true|false)$/)
15
+
16
+ const ZERO = 48
17
+ const NINE = 57
18
+ const A = 65
19
+ const Z = 90
20
+ const LF = 10
21
+ const CR = 13
22
+ const TAB = 9
23
+ const SPACE = 32
24
+ const PLUS = 43
25
+ const MINUS = 45
26
+ const DOT = 46
27
+ const UNDERSCORE = 95
28
+ const SLASH = 47
29
+ const STAR = 42
30
+
31
+ const isDigit = (c: number) => ZERO <= c && c <= NINE
32
+ const isAlpha = (c: number) => ((c &= ~32), A <= c && c <= Z)
33
+ const isLine = (c: number) => c === LF || c === CR
34
+ const isSpace = (c: number) => isLine(c) || c === TAB || c === SPACE
35
+ const isNumber = (c: number) => isAlpha(c) || isDigit(c) || c === PLUS || c === MINUS || c === DOT
36
+ const isIdent = (c: number) => isAlpha(c) || isDigit(c) || c === UNDERSCORE
37
+
10
38
  /**
11
- * Tokenizes a string of GLSL code.
39
+ * Tokenizes a string of GLSL or WGSL code.
12
40
  */
13
41
  export function tokenize(code: string, index: number = 0): Token[] {
14
42
  const tokens: Token[] = []
15
43
 
44
+ const KEYWORDS = isWGSL(code) ? WGSL_KEYWORDS : GLSL_KEYWORDS
16
45
  while (index < code.length) {
17
- let char = code[index]
18
- let value = ''
19
-
20
- // Skip whitespace
21
- if (/\s/.test(char)) {
22
- index++
23
- continue
24
- }
25
-
26
- // Escape newlines after directives, skip comments
27
- if (char === '#') {
28
- let j = index
29
- while (code[j] !== '\n' && !/\/[\/\*]/.test(code[j] + code[j + 1])) j++
30
- code = code.substring(0, j) + '\\' + code.substring(j)
31
- }
32
-
33
- // Parse values and identifiers
34
- while ((/\d/.test(char) ? /[\d\.\-\w]/ : /\w/).test(code[index])) value += code[index++]
46
+ let value = code[index]
47
+ const char = code.charCodeAt(index++)
35
48
 
36
- // Parse symbols, combine if able
37
- if (!value) {
38
- value = char
39
- for (const symbol of GLSL_SYMBOLS) {
40
- if (symbol.length > value.length && code.startsWith(symbol, index)) value = symbol
41
- }
42
- index += value.length
43
- }
44
-
45
- // Label and add token
46
- if (/\/[\/\*]/.test(value)) {
47
- const isMultiline = value === '/*'
49
+ if (isSpace(char)) {
50
+ while (isSpace(code.charCodeAt(index))) value += code[index++]
51
+ tokens.push({ type: 'whitespace', value })
52
+ } else if (isDigit(char)) {
53
+ while (isNumber(code.charCodeAt(index))) value += code[index++]
54
+ if (isFloat(value)) tokens.push({ type: 'float', value })
55
+ else tokens.push({ type: 'int', value })
56
+ } else if (isAlpha(char)) {
57
+ while (isIdent(code.charCodeAt(index))) value += code[index++]
58
+ if (isBool(value)) tokens.push({ type: 'bool', value })
59
+ else if (KEYWORDS.includes(value)) tokens.push({ type: 'keyword', value })
60
+ else tokens.push({ type: 'identifier', value })
61
+ } else if (char === SLASH && (code.charCodeAt(index) === SLASH || code.charCodeAt(index) === STAR)) {
62
+ const isMultiline = code.charCodeAt(index) === STAR
48
63
  while (!value.endsWith(isMultiline ? '*/' : '\n')) value += code[index++]
49
64
  tokens.push({ type: 'comment', value })
50
- } else if (!/\w/.test(char)) {
51
- tokens.push({ type: 'symbol', value })
52
- } else if (/\d/.test(char)) {
53
- if (/[uU]/.test(value)) tokens.push({ type: 'uint', value })
54
- else if (/\.|[eE]-?\d/.test(value)) tokens.push({ type: 'float', value })
55
- else tokens.push({ type: 'int', value })
56
- } else if (/^(true|false)$/.test(value)) {
57
- tokens.push({ type: 'bool', value })
58
- } else if (GLSL_KEYWORDS.includes(tokens[tokens.length - 1]?.value === '#' ? `#${value}` : value)) {
59
- tokens.push({ type: 'keyword', value })
60
65
  } else {
61
- tokens.push({ type: 'identifier', value })
66
+ for (const symbol of SYMBOLS) {
67
+ if (symbol.length > value.length && code.startsWith(symbol, index - 1)) value = symbol
68
+ }
69
+ index += value.length - 1
70
+ tokens.push({ type: 'symbol', value })
62
71
  }
63
72
  }
64
73