From a60d02044bafd6904197e4cbbd1aa9272a41c6eb Mon Sep 17 00:00:00 2001 From: Pham Quang Nghi Date: Tue, 13 Oct 2026 20:46:17 +0000 Subject: [PATCH] fix(preprocess): optimize file patching and fix edge-case bugs --- src/preprocess/preprocess.go | 1543 +++++++++++++++++----------------- src/utils/utils.go | 156 ++-- 2 files changed, 835 insertions(+), 864 deletions(-) diff --git a/src/preprocess/preprocess.go b/src/preprocess/preprocess.go index dcf0c535ef..c9f2facc5b 100644 --- a/src/preprocess/preprocess.go +++ b/src/preprocess/preprocess.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "os" "path" @@ -13,6 +14,7 @@ import ( "runtime" "strconv" "strings" + "sync" "github.com/pterm/pterm" "github.com/spicetify/cli/src/utils" @@ -56,12 +58,12 @@ func readRemoteCssMap(tag string, cssTranslationMap *map[string]string) error { cssMapResp, err := http.Get(cssMapURL) if err != nil { return err - } else { - err := json.NewDecoder(cssMapResp.Body).Decode(cssTranslationMap) - if err != nil { - utils.PrintWarning("Remote CSS map JSON malformed.") - return err - } + } + defer cssMapResp.Body.Close() + + if err := json.NewDecoder(cssMapResp.Body).Decode(cssTranslationMap); err != nil { + utils.PrintWarning("Remote CSS map JSON malformed.") + return err } return nil } @@ -107,12 +109,14 @@ func Start(version string, spotifyBasePath string, extractedAppsPath string, fla } cssMapPairs := make([]string, 0, len(cssTranslationMap)*4) + cssMapCSSPairs := make([]string, 0, len(cssTranslationMap)*2) for k, v := range cssTranslationMap { cssMapPairs = append(cssMapPairs, k+":", `"`+v+`":`) cssMapPairs = append(cssMapPairs, k, v) + cssMapCSSPairs = append(cssMapCSSPairs, k, v) } cssMapJSReplacer := strings.NewReplacer(cssMapPairs...) - cssMapBareKeySpaceRe := regexp.MustCompile(`\b[a-zA-Z0-9_]{16,21}[ \t]+:`) + cssMapCSSReplacer := strings.NewReplacer(cssMapCSSPairs...) verParts := strings.Split(flags.SpotifyVer, ".") spotifyMajor, spotifyMinor, spotifyPatch := 0, 0, 0 @@ -195,11 +199,11 @@ func Start(version string, spotifyBasePath string, extractedAppsPath string, fla } var filesToPatch []string - filepath.Walk(appPath, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { + filepath.WalkDir(appPath, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { return nil } - ext := filepath.Ext(info.Name()) + ext := filepath.Ext(d.Name()) if ext == ".js" || ext == ".css" || ext == ".html" { filesToPatch = append(filesToPatch, path) } @@ -214,90 +218,94 @@ func Start(version string, spotifyBasePath string, extractedAppsPath string, fla WithTitleStyle(pterm.NewStyle(pterm.Bold)). WithShowCount(true). Start() + + var wg sync.WaitGroup + var barMu sync.Mutex + sem := make(chan struct{}, runtime.NumCPU()) + for _, path := range filesToPatch { - info, err := os.Stat(path) - if err != nil { - continue - } - fileName := info.Name() - extension := filepath.Ext(fileName) - - switch extension { - case ".js": - utils.ModifyFile(path, func(content string) string { - if flags.DisableSentry && (fileName == "xpui.js" || fileName == "xpui-snapshot.js") { - content = disableSentry(content) - } + wg.Add(1) + sem <- struct{}{} - if flags.DisableLogging { - content = disableLogging(content) - } + go func(path string) { + defer wg.Done() + defer func() { <-sem }() + + fileName := filepath.Base(path) + extension := filepath.Ext(fileName) - if flags.ExposeAPIs { - switch fileName { - case "xpui-modules.js", "xpui-snapshot.js": - content = exposeAPIs_main(content) - content = exposeAPIs_vendor(content) - case "xpui.js": - content = exposeAPIs_main(content) - if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 { + switch extension { + case ".js": + utils.ModifyFile(path, func(content string) string { + if flags.DisableSentry && (fileName == "xpui.js" || fileName == "xpui-snapshot.js") { + content = disableSentry(content) + } + + if flags.DisableLogging { + content = disableLogging(content) + } + + if flags.ExposeAPIs { + switch fileName { + case "xpui-modules.js", "xpui-snapshot.js": + content = exposeAPIs_main(content) + content = exposeAPIs_vendor(content) + case "xpui.js": + content = exposeAPIs_main(content) + if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 { + content = exposeAPIs_vendor(content) + } + case "vendor~xpui.js": content = exposeAPIs_vendor(content) } - case "vendor~xpui.js": - content = exposeAPIs_vendor(content) - } - if spotifyMajor >= 1 && spotifyMinor >= 2 && (spotifyPatch >= 28 && spotifyPatch <= 57) { - utils.ReplaceOnce(&content, `(typeName\])`, func(submatches ...string) string { - return fmt.Sprintf(`%s || []`, submatches[1]) - }) + if spotifyMajor >= 1 && spotifyMinor >= 2 && (spotifyPatch >= 28 && spotifyPatch <= 57) { + utils.ReplaceOnce(&content, `(typeName\])`, func(submatches ...string) string { + return fmt.Sprintf(`%s || []`, submatches[1]) + }) + } + + // to avoid syntaxerror on Spotify 1.2.78 and above + if spotifyMajor == 1 && spotifyMinor == 2 && spotifyPatch < 78 { + utils.ReplaceOnce(&content, `\(\({[^}]*,\s*imageSrc`, func(submatches ...string) string { + return fmt.Sprintf("Spicetify.Snackbar.enqueueImageSnackbar=%s", submatches[0]) + }) + } + + content = additionalPatches(content) } - // to avoid syntaxerror on Spotify 1.2.78 and above - if spotifyMajor == 1 && spotifyMinor == 2 && spotifyPatch < 78 { - utils.ReplaceOnce(&content, `\(\({[^}]*,\s*imageSrc`, func(submatches ...string) string { - return fmt.Sprintf("Spicetify.Snackbar.enqueueImageSnackbar=%s", submatches[0]) + if fileName == "dwp-top-bar.js" || fileName == "dwp-now-playing-bar.js" || fileName == "dwp-home-chips-row.js" { + utils.ReplaceOnce(&content, `e\.state\.cinemaState`, func(submatches ...string) string { + return "e.state?.cinemaState" }) } - content = additionalPatches(content) - } - - if fileName == "dwp-top-bar.js" || fileName == "dwp-now-playing-bar.js" || fileName == "dwp-home-chips-row.js" { - utils.ReplaceOnce(&content, `e\.state\.cinemaState`, func(submatches ...string) string { - return "e.state?.cinemaState" + // Bare keys with whitespace before the colon + // the replacer's k+":" entry can't match these, so handle them first + content = cssMapBareKeySpaceRe.ReplaceAllStringFunc(content, func(match string) string { + colonIdx := strings.LastIndex(match, ":") + key := strings.TrimRight(match[:colonIdx], " \t") + if v, ok := cssTranslationMap[key]; ok { + return `"` + v + `":` + } + return match }) - } + // Single pass: k+":" → "v": for bare keys, + // k -> v for all other occurrences + content = cssMapJSReplacer.Replace(content) + content = colorVariableReplaceForJS(content) - // Bare keys with whitespace before the colon - // the replacer's k+":" entry can't match these, so handle them first - content = cssMapBareKeySpaceRe.ReplaceAllStringFunc(content, func(match string) string { - colonIdx := strings.LastIndex(match, ":") - key := strings.TrimRight(match[:colonIdx], " \t") - if v, ok := cssTranslationMap[key]; ok { - return `"` + v + `":` - } - return match + return content }) - // Single pass: k+":" → "v": for bare keys, - // k -> v for all other occurrences - content = cssMapJSReplacer.Replace(content) - content = colorVariableReplaceForJS(content) - - return content - }) - case ".css": - utils.ModifyFile(path, func(content string) string { - for k, v := range cssTranslationMap { - utils.Replace(&content, k, func(submatches ...string) string { - return v - }) - } - if flags.RemoveRTL { - content = removeRTL(content) - } - if fileName == "xpui.css" || fileName == "xpui-snapshot.css" { - content = content + ` + case ".css": + utils.ModifyFile(path, func(content string) string { + content = cssMapCSSReplacer.Replace(content) + if flags.RemoveRTL { + content = removeRTL(content) + } + if fileName == "xpui.css" || fileName == "xpui-snapshot.css" { + content = content + ` :where(.spicetify-topbar-button){align-items:center;display:flex} .spicetify-topbar-button>button{align-items:center;display:flex;height:32px;justify-content:center;padding:0;width:32px} :where(.main-addButton-button){align-items:center;background:transparent;border:0;color:rgba(var(--spice-rgb-text),.7);display:flex;height:32px;justify-content:center;padding:0;width:32px} @@ -308,277 +316,226 @@ func Start(version string, spotifyBasePath string, extractedAppsPath string, fla :where(.main-gridContainer-gridContainer){display:grid;grid-gap:var(--grid-gap);grid-auto-rows:min-content;grid-template-columns:repeat(auto-fill,minmax(var(--min-column-width),1fr))} .main-gridContainer-fixedWidth{grid-template-columns:repeat(auto-fill,var(--column-width));width:calc((var(--column-count) - 1) * var(--grid-gap) + var(--column-count) * var(--column-width))}.main-cardImage-imageWrapper{background-color:var(--card-color,#333);border-radius:var(--card-image-radius,6px);box-shadow:0 8px 24px rgba(0,0,0,.5);padding-bottom:100%;position:relative;width:100%}.main-cardImage-circular{--card-image-radius:50%}.main-card-imagePlaceholder,.main-cardImage-image{height:100%;left:0;position:absolute;top:0;width:100%}.main-card-card{border-radius:8px;-webkit-box-flex:1;background:var(--spice-player);-ms-flex:1;flex:1;isolation:isolate;padding:16px;position:relative;-webkit-transition:background-color .3s;transition:background-color .3s;width:100%}.main-card-card:hover,.main-card-card[data-context-menu-open=true]{background:var(--spice-card)}.main-card-card:focus-within{background:var(--spice-card)}.main-card-card .main-card-cardLink{position:absolute;z-index:0}.RjYPjR7FsVf42b3a5Efm:before,.Z78JmW4GUvvxLXSFjb7R:not(:last-child):after{content:"\2022";margin:0 4px}.main-card-card a,.main-card-card button{position:relative;z-index:1}.main-card-cardLink{bottom:0;content:"";cursor:pointer;left:0;overflow:hidden;right:0;text-indent:100%;top:0;white-space:nowrap;z-index:0}.main-card-cardTitle{z-index:1}.main-card-cardTitleLink:focus,.main-card-cardTitleLink:hover{text-decoration:none}.Wmr5qZ5jui6X37XCrChA{opacity:0}.main-card-newEpisodeIndicator{margin-top:3px}.main-card-imageContainer{pointer-events:none;position:relative}.main-card-imageContainerOld,.main-card-imageContainerSkeleton{margin-bottom:16px}.main-card-cardMetadata{min-height:62px;line-clamp:2;-webkit-line-clamp:2;overflow:hidden;text-overflow:ellipsis}.main-card-DownloadStatusIndicator{bottom:12px;inset-inline-end:12px;position:absolute}.main-card-PlayButtonContainer{border-radius:500px;bottom:8px;-webkit-box-shadow:0 8px 8px rgba(var(--spice-rgb-shadow),.3);box-shadow:0 8px 8px rgba(var(--spice-rgb-shadow),.3);inset-inline-end:8px;opacity:0;pointer-events:none;position:absolute;-webkit-transform:translateY(8px);transform:translateY(8px);z-index:2}@media(pointer:coarse){.main-card-PlayButtonContainer{display:none}}@media(prefers-reduced-motion:no-preference){.main-card-PlayButtonContainer{-webkit-transition:opacity .2s ease-out,-webkit-transform .2s ease-out;transition:transform .2s ease-out,opacity .2s ease-out,-webkit-transform .2s ease-out}}.main-card-PlayButtonContainerVisible{opacity:1;pointer-events:auto;position:absolute;-webkit-transform:translateY(0);transform:translateY(0)}.main-card-cardContainer{--animation-speed:0.2s!important;white-space:normal}.main-card-card:hover .main-card-PlayButtonContainer,.main-card-cardContainer:hover .main-card-PlayButtonContainer{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}.main-card-card:focus-within .main-card-PlayButtonContainer,.main-card-cardContainer:focus-within{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}@media(pointer:fine){.main-card-card .main-card-cardLink:hover{cursor:pointer}.main-card-card:hover .main-card-PlayButtonContainer,.main-card-cardContainer:hover .main-card-PlayButtonContainer{opacity:1;pointer-events:auto;position:absolute}.main-card-card:focus-within .main-card-PlayButtonContainer,.main-card-cardContainer:focus-within .main-card-PlayButtonContainer{opacity:1;pointer-events:auto;position:absolute}}.main-card-cardMetadata,.v3isO2phyJAoZRkmme0G{display:-webkit-box;-webkit-box-orient:vertical} ` - content += legacyFixedWidthGridCSS - } - return content - }) + content += legacyFixedWidthGridCSS + } + return content + }) - case ".html": - utils.ModifyFile(path, func(content string) string { - var tags string - tags += "\n" - tags += "\n" + case ".html": + utils.ModifyFile(path, func(content string) string { + var tags string + tags += "\n" + tags += "\n" - if flags.ExposeAPIs { - tags += "\n" - tags += "\n" - } + if flags.ExposeAPIs { + tags += "\n" + tags += "\n" + } - utils.Replace(&content, ``, func(submatches ...string) string { - return fmt.Sprintf("%s\n%s", submatches[0], tags) - }) + utils.Replace(&content, ``, func(submatches ...string) string { + return fmt.Sprintf("%s\n%s", submatches[0], tags) + }) - return content - }) - } + return content + }) + } - bar.Increment() + barMu.Lock() + bar.Increment() + barMu.Unlock() + }(path) } + wg.Wait() } // StartCSS modifies all CSS files in extractedAppsPath to change // all colors value with CSS variables. func StartCSS(extractedAppsPath string) { appPath := filepath.Join(extractedAppsPath, "xpui") - filepath.Walk(appPath, func(path string, info os.FileInfo, err error) error { + var cssFiles []string + + filepath.WalkDir(appPath, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + name := d.Name() // temp so text won't be black ._. - if strings.HasPrefix(info.Name(), "pip-mini-player") && strings.HasSuffix(info.Name(), ".css") { + if strings.HasPrefix(name, "pip-mini-player") && strings.HasSuffix(name, ".css") { return nil } - if filepath.Ext(info.Name()) == ".css" { - utils.ModifyFile(path, func(content string) string { - return colorVariableReplace(content) - }) + if filepath.Ext(name) == ".css" { + cssFiles = append(cssFiles, path) } return nil }) -} -func colorVariableReplace(content string) string { - hexAlphaReplacement := func(maxValue float64) func(submatches ...string) string { - return func(submatches ...string) string { - alpha, err := strconv.ParseUint(submatches[1], 16, 8) - if err != nil { - return submatches[0] - } + var wg sync.WaitGroup + sem := make(chan struct{}, runtime.NumCPU()) - return fmt.Sprintf( - "rgba(var(--spice-rgb-text),%s)", - strconv.FormatFloat(float64(alpha)/maxValue, 'f', -1, 64), - ) - } + for _, path := range cssFiles { + wg.Add(1) + sem <- struct{}{} + + go func(path string) { + defer wg.Done() + defer func() { <-sem }() + utils.ModifyFile(path, colorVariableReplace) + }(path) } + wg.Wait() +} - colorPatches := []Patch{ - { - Name: "CSS: --spice-player", - Regex: `#(181818|212121)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-player)" - }, - }, - { - Name: "CSS: --spice-card", - Regex: `#282828\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-card)" - }, - }, - { - Name: "CSS: --spice-main-elevated", - Regex: `#(242424|1f1f1f)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-main-elevated)" - }, - }, - { - Name: "CSS: --spice-main", - Regex: `#121212\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-main)" - }, - }, - { - Name: "CSS: --spice-card-elevated", - Regex: `#(242424|1f1f1f)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-card-elevated)" - }, - }, - { - Name: "CSS: --spice-highlight", - Regex: `#1a1a1a\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-highlight)" - }, - }, - { - Name: "CSS: --spice-highlight-elevated", - Regex: `#2a2a2a\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-highlight-elevated)" - }, - }, - { - Name: "CSS: --spice-sidebar", - Regex: `#(000|000000)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-sidebar)" - }, - }, - { - Name: "CSS: --spice-rgb-text (8-digit hex)", - Regex: `(?i)#ffffff([0-9a-f]{2})\b`, - Replacement: hexAlphaReplacement(255), - }, - { - Name: "CSS: --spice-rgb-text (4-digit hex)", - Regex: `(?i)#fff([0-9a-f])\b`, - Replacement: hexAlphaReplacement(15), - }, - { - Name: "CSS: --spice-text", - Regex: `(white;|#fff|#ffffff|#f8f8f8)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-text)" - }, - }, - { - Name: "CSS: --spice-subtext", - Regex: `#(b3b3b3|a7a7a7)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-subtext)" - }, - }, - { - Name: "CSS: --spice-button", - Regex: `#(1db954|1877f2)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-button)" - }, - }, - { - Name: "CSS: --spice-button-active", - Regex: `#(1ed760|1fdf64|169c46)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-button-active)" - }, - }, - { - Name: "CSS: --spice-button-disabled", - Regex: `#535353\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-button-disabled)" - }, - }, - { - Name: "CSS: --spice-tab-active", - Regex: `#(333|333333)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-tab-active)" - }, - }, - { - Name: "CSS: --spice-misc", - Regex: `#7f7f7f\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-misc)" - }, - }, - { - Name: "CSS: --spice-notification", - Regex: `#(4687d6|2e77d0)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-notification)" - }, - }, - { - Name: "CSS: --spice-notification-error", - Regex: `#(e22134|cd1a2b)\b`, - Replacement: func(submatches ...string) string { - return "var(--spice-notification-error)" - }, - }, - { - Name: "CSS (rgba): --spice-rgb-text", - Regex: `(?i)rgba\(\s*255\s*,\s*255\s*,\s*255\s*,`, - Replacement: func(submatches ...string) string { - return "rgba(var(--spice-rgb-text)," - }, - }, - { - Name: "CSS (rgba): --spice-main", - Regex: `rgba\(18,18,18,([\d\.]+)\)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("rgba(var(--spice-main),%s)", submatches[1]) - }, - }, - { - Name: "CSS (rgba): --spice-card", - Regex: `rgba\(40,40,40,([\d\.]+)\)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("rgba(var(--spice-card),%s)", submatches[1]) - }, - }, - { - Name: "CSS (rgba): --spice-rgb-shadow", - Regex: `rgba\(0,0,0,([\d\.]+)\)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("rgba(var(--spice-rgb-shadow),%s)", submatches[1]) - }, - }, - { - Name: "CSS (hsla): --spice-rgb-text", - Regex: `hsla\(0,0%,100%,\.9\)`, - Replacement: func(submatches ...string) string { - return "rgba(var(--spice-rgb-text),.9)" - }, - }, - { - Name: "CSS (hsla): --spice-rgb-selected-row", - Regex: `hsla\(0,0%,100%,([\d\.]+)\)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("rgba(var(--spice-rgb-selected-row),%s)", submatches[1]) - }, - }, +func hexAlphaReplacement(maxValue float64) func(submatches ...string) string { + return func(submatches ...string) string { + alpha, err := strconv.ParseUint(submatches[1], 16, 8) + if err != nil { + return submatches[0] + } + + return fmt.Sprintf( + "rgba(var(--spice-rgb-text),%s)", + strconv.FormatFloat(float64(alpha)/maxValue, 'f', -1, 64), + ) } +} + +var cssHexColorMap = map[string]string{ + "#181818": "var(--spice-player)", + "#212121": "var(--spice-player)", + "#282828": "var(--spice-card)", + "#242424": "var(--spice-main-elevated)", + "#1f1f1f": "var(--spice-main-elevated)", + "#121212": "var(--spice-main)", + "#1a1a1a": "var(--spice-highlight)", + "#2a2a2a": "var(--spice-highlight-elevated)", + "#000": "var(--spice-sidebar)", + "#000000": "var(--spice-sidebar)", + "#b3b3b3": "var(--spice-subtext)", + "#a7a7a7": "var(--spice-subtext)", + "#1db954": "var(--spice-button)", + "#1877f2": "var(--spice-button)", + "#1ed760": "var(--spice-button-active)", + "#1fdf64": "var(--spice-button-active)", + "#169c46": "var(--spice-button-active)", + "#535353": "var(--spice-button-disabled)", + "#333": "var(--spice-tab-active)", + "#333333": "var(--spice-tab-active)", + "#7f7f7f": "var(--spice-misc)", + "#4687d6": "var(--spice-notification)", + "#2e77d0": "var(--spice-notification)", + "#e22134": "var(--spice-notification-error)", + "#cd1a2b": "var(--spice-notification-error)", +} + +var colorPatches = []Patch{ + { + Name: "CSS: hex color variables", + Regex: `#(181818|212121|282828|242424|1f1f1f|121212|1a1a1a|2a2a2a|000000|000|b3b3b3|a7a7a7|1db954|1877f2|1ed760|1fdf64|169c46|535353|333333|333|7f7f7f|4687d6|2e77d0|e22134|cd1a2b)\b`, + Replacement: func(submatches ...string) string { + if v, ok := cssHexColorMap[submatches[0]]; ok { + return v + } + return submatches[0] + }, + }, + { + Name: "CSS: --spice-rgb-text (8-digit hex)", + Regex: `(?i)#ffffff([0-9a-f]{2})\b`, + Replacement: hexAlphaReplacement(255), + }, + { + Name: "CSS: --spice-rgb-text (4-digit hex)", + Regex: `(?i)#fff([0-9a-f])\b`, + Replacement: hexAlphaReplacement(15), + }, + { + Name: "CSS: --spice-text", + Regex: `(white;|#fff|#ffffff|#f8f8f8)\b`, + Replacement: func(submatches ...string) string { + return "var(--spice-text)" + }, + }, + { + Name: "CSS (rgba): --spice-rgb-text", + Regex: `(?i)rgba\(\s*255\s*,\s*255\s*,\s*255\s*,`, + Replacement: func(submatches ...string) string { + return "rgba(var(--spice-rgb-text)," + }, + }, + { + Name: "CSS (rgba): --spice-main", + Regex: `rgba\(18,18,18,([\d\.]+)\)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-main),%s)", submatches[1]) + }, + }, + { + Name: "CSS (rgba): --spice-card", + Regex: `rgba\(40,40,40,([\d\.]+)\)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-card),%s)", submatches[1]) + }, + }, + { + Name: "CSS (rgba): --spice-rgb-shadow", + Regex: `rgba\(0,0,0,([\d\.]+)\)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-rgb-shadow),%s)", submatches[1]) + }, + }, + { + Name: "CSS (hsla): --spice-rgb-text", + Regex: `hsla\(0,0%,100%,\.9\)`, + Replacement: func(submatches ...string) string { + return "rgba(var(--spice-rgb-text),.9)" + }, + }, + { + Name: "CSS (hsla): --spice-rgb-selected-row", + Regex: `hsla\(0,0%,100%,([\d\.]+)\)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("rgba(var(--spice-rgb-selected-row),%s)", submatches[1]) + }, + }, +} +func colorVariableReplace(content string) string { return applyPatches(content, colorPatches) } -func colorVariableReplaceForJS(content string) string { - colorVariablePatches := []Patch{ - { - Name: "CSS (JS): --spice-button", - Regex: `"#1db954"`, - Replacement: func(submatches ...string) string { - return ` getComputedStyle(document.body).getPropertyValue("--spice-button").trim()` - }, - }, - { - Name: "CSS (JS): --spice-subtext", - Regex: `"#b3b3b3"`, - Replacement: func(submatches ...string) string { - return ` getComputedStyle(document.body).getPropertyValue("--spice-subtext").trim()` - }, - }, - { - Name: "CSS (JS): --spice-text", - Regex: `"#ffffff"`, - Replacement: func(submatches ...string) string { - return ` getComputedStyle(document.body).getPropertyValue("--spice-text").trim()` - }, - }, - { - Name: "CSS (JS): --spice-text white", - Regex: `color:"white"`, - Replacement: func(submatches ...string) string { - return `color:"var(--spice-text)"` - }, - }, - } +var colorVariablePatches = []Patch{ + { + Name: "CSS (JS): --spice-button", + Regex: `"#1db954"`, + Replacement: func(submatches ...string) string { + return ` getComputedStyle(document.body).getPropertyValue("--spice-button").trim()` + }, + }, + { + Name: "CSS (JS): --spice-subtext", + Regex: `"#b3b3b3"`, + Replacement: func(submatches ...string) string { + return ` getComputedStyle(document.body).getPropertyValue("--spice-subtext").trim()` + }, + }, + { + Name: "CSS (JS): --spice-text", + Regex: `"#ffffff"`, + Replacement: func(submatches ...string) string { + return ` getComputedStyle(document.body).getPropertyValue("--spice-text").trim()` + }, + }, + { + Name: "CSS (JS): --spice-text white", + Regex: `color:"white"`, + Replacement: func(submatches ...string) string { + return `color:"var(--spice-text)"` + }, + }, +} +func colorVariableReplaceForJS(content string) string { return applyPatches(content, colorVariablePatches) } @@ -593,331 +550,335 @@ func disableSentry(input string) string { return input } +var loggingPatches = []Patch{ + { + Name: "Remove sp://logging/v3/*", + Regex: `sp://logging/v3/\w+`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Remove /v3/events endpoints", + Regex: `[^"\/]+\/[^"\/]+\/(public\/)?v3\/events`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Disable registerEventListeners", + Regex: `key:"registerEventListeners",value:function\(\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable logInteraction", + Regex: `key:"logInteraction",value:function\([\w,]+\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) + }, + }, + { + Name: "Disable logNonAuthInteraction", + Regex: `key:"logNonAuthInteraction",value:function\([\w,]+\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) + }, + }, + { + Name: "Disable logImpression", + Regex: `key:"logImpression",value:function\([\w,]+\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable logNonAuthImpression", + Regex: `key:"logNonAuthImpression",value:function\([\w,]+\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable logNavigation", + Regex: `key:"logNavigation",value:function\([\w,]+\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable handleBackgroundStates", + Regex: `key:"handleBackgroundStates",value:function\(\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable createLoggingParams", + Regex: `key:"createLoggingParams",value:function\([\w,]+\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable initSendingEvents", + Regex: `key:"initSendingEvents",value:function\(\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable flush", + Regex: `key:"flush",value:function\(\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable send", + Regex: `(\{key:"send",value:function\([\w,]+\))\{[\d\w\s,{}()[\]\.,!\?=>&|;:_""]+?\}(\},\{key:"hasContext")`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%s{return;}%s", submatches[1], submatches[2]) + }, + }, + { + Name: "Disable lastFlush", + Regex: `key:"lastFlush",value:function\(\)\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn Promise.resolve({fired:true});", submatches[0]) + }, + }, + { + Name: "Disable addItemInEventsStorage", + Regex: `key:"addItemInEventsStorage",value:function\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable createLoggingParams (new)", + Regex: `key:"createLoggingParams",value:function\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn {interactionIds:null,pageInstanceIds:null};", submatches[0]) + }, + }, + { + Name: "Disable addEventsToESSData", + Regex: `key:"addEventsToESSData",value:function\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable registerEventListeners (new)", + Regex: `registerEventListeners\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable logInteraction (new)", + Regex: `logInteraction\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) + }, + }, + { + Name: "Disable logImpression (new)", + Regex: `logImpression\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable logNavigation (new)", + Regex: `logNavigation\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable handleBackgroundStates (new)", + Regex: `handleBackgroundStates\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable initSendingEvents (new)", + Regex: `initSendingEvents\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable sendEvents", + Regex: `sendEvents\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable storeEvent", + Regex: `storeEvent\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable lastFlush (new)", + Regex: `lastFlush\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn Promise.resolve({fired:true});", submatches[0]) + }, + }, + { + Name: "Disable addItemInEventsStorage (new)", + Regex: `addItemInEventsStorage\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, + { + Name: "Disable createLoggingParams (new)", + Regex: `createLoggingParams\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn {interactionIds:null,pageInstanceIds:null};", submatches[0]) + }, + }, + { + Name: "Disable addEventsToESSData (new)", + Regex: `addEventsToESSData\([^)]*\)\s*\{`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sreturn;", submatches[0]) + }, + }, +} + func disableLogging(input string) string { - loggingPatches := []Patch{ - { - Name: "Remove sp://logging/v3/*", - Regex: `sp://logging/v3/\w+`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Remove /v3/events endpoints", - Regex: `[^"\/]+\/[^"\/]+\/(public\/)?v3\/events`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Disable registerEventListeners", - Regex: `key:"registerEventListeners",value:function\(\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable logInteraction", - Regex: `key:"logInteraction",value:function\([\w,]+\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) - }, - }, - { - Name: "Disable logNonAuthInteraction", - Regex: `key:"logNonAuthInteraction",value:function\([\w,]+\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) - }, - }, - { - Name: "Disable logImpression", - Regex: `key:"logImpression",value:function\([\w,]+\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable logNonAuthImpression", - Regex: `key:"logNonAuthImpression",value:function\([\w,]+\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable logNavigation", - Regex: `key:"logNavigation",value:function\([\w,]+\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable handleBackgroundStates", - Regex: `key:"handleBackgroundStates",value:function\(\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable createLoggingParams", - Regex: `key:"createLoggingParams",value:function\([\w,]+\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable initSendingEvents", - Regex: `key:"initSendingEvents",value:function\(\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable flush", - Regex: `key:"flush",value:function\(\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable send", - Regex: `(\{key:"send",value:function\([\w,]+\))\{[\d\w\s,{}()[\]\.,!\?=>&|;:_""]+?\}(\},\{key:"hasContext")`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%s{return;}%s", submatches[1], submatches[2]) - }, - }, - { - Name: "Disable lastFlush", - Regex: `key:"lastFlush",value:function\(\)\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn Promise.resolve({fired:true});", submatches[0]) - }, - }, - { - Name: "Disable addItemInEventsStorage", - Regex: `key:"addItemInEventsStorage",value:function\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable createLoggingParams (new)", - Regex: `key:"createLoggingParams",value:function\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn {interactionIds:null,pageInstanceIds:null};", submatches[0]) - }, - }, - { - Name: "Disable addEventsToESSData", - Regex: `key:"addEventsToESSData",value:function\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable registerEventListeners (new)", - Regex: `registerEventListeners\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable logInteraction (new)", - Regex: `logInteraction\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn {interactionId:null,pageInstanceId:null};", submatches[0]) - }, - }, - { - Name: "Disable logImpression (new)", - Regex: `logImpression\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable logNavigation (new)", - Regex: `logNavigation\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable handleBackgroundStates (new)", - Regex: `handleBackgroundStates\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable initSendingEvents (new)", - Regex: `initSendingEvents\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable sendEvents", - Regex: `sendEvents\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable storeEvent", - Regex: `storeEvent\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable lastFlush (new)", - Regex: `lastFlush\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn Promise.resolve({fired:true});", submatches[0]) - }, - }, - { - Name: "Disable addItemInEventsStorage (new)", - Regex: `addItemInEventsStorage\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - { - Name: "Disable createLoggingParams (new)", - Regex: `createLoggingParams\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn {interactionIds:null,pageInstanceIds:null};", submatches[0]) - }, - }, - { - Name: "Disable addEventsToESSData (new)", - Regex: `addEventsToESSData\([^)]*\)\s*\{`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sreturn;", submatches[0]) - }, - }, - } return applyPatches(input, loggingPatches) } -func removeRTL(input string) string { - rtlPatches := []Patch{ - { - Name: "Remove }[dir=ltr]", - Regex: `}\[dir=ltr\]\s?`, - Replacement: func(submatches ...string) string { - return "} " - }, - }, - { - Name: "Remove html[dir=ltr]", - Regex: `html\[dir=ltr\]`, - Replacement: func(submatches ...string) string { - return "html" - }, - }, - { - Name: "Remove ', [dir=rtl]' selectors", - Regex: `,\s?\[dir=rtl\].+?(\{.+?\})`, - Replacement: func(submatches ...string) string { - return submatches[1] - }, - }, - { - Name: "Remove [something][dir=rtl] blocks", - Regex: `[\w\-\.]+\[dir=rtl\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Remove }[lang=ar] blocks", - Regex: `\}\[lang=ar\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "}" - }, - }, - { - Name: "Remove }[dir=rtl] blocks", - Regex: `\}\[dir=rtl\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "}" - }, - }, - { - Name: "Remove }html[dir=rtl] blocks", - Regex: `\}html\[dir=rtl\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "}" - }, - }, - { - Name: "Remove }html[lang=ar] blocks", - Regex: `\}html\[lang=ar\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "}" - }, - }, - { - Name: "Remove [lang=ar] blocks", - Regex: `\[lang=ar\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Remove html[dir=rtl] blocks", - Regex: `html\[dir=rtl\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Remove html[lang=ar] blocks", - Regex: `html\[lang=ar\].+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Remove [dir=rtl] blocks", - Regex: `\[dir=rtl\][^)]+?\{.+?\}`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - } +var rtlPatches = []Patch{ + { + Name: "Remove }[dir=ltr]", + Regex: `}\[dir=ltr\]\s?`, + Replacement: func(submatches ...string) string { + return "} " + }, + }, + { + Name: "Remove html[dir=ltr]", + Regex: `html\[dir=ltr\]`, + Replacement: func(submatches ...string) string { + return "html" + }, + }, + { + Name: "Remove ', [dir=rtl]' selectors", + Regex: `,\s?\[dir=rtl\].+?(\{.+?\})`, + Replacement: func(submatches ...string) string { + return submatches[1] + }, + }, + { + Name: "Remove [something][dir=rtl] blocks", + Regex: `[\w\-\.]+\[dir=rtl\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Remove }[lang=ar] blocks", + Regex: `\}\[lang=ar\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "}" + }, + }, + { + Name: "Remove }[dir=rtl] blocks", + Regex: `\}\[dir=rtl\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "}" + }, + }, + { + Name: "Remove }html[dir=rtl] blocks", + Regex: `\}html\[dir=rtl\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "}" + }, + }, + { + Name: "Remove }html[lang=ar] blocks", + Regex: `\}html\[lang=ar\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "}" + }, + }, + { + Name: "Remove [lang=ar] blocks", + Regex: `\[lang=ar\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Remove html[dir=rtl] blocks", + Regex: `html\[dir=rtl\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Remove html[lang=ar] blocks", + Regex: `html\[lang=ar\].+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Remove [dir=rtl] blocks", + Regex: `\[dir=rtl\][^)]+?\{.+?\}`, + Replacement: func(submatches ...string) string { + return "" + }, + }, +} +func removeRTL(input string) string { return applyPatches(input, rtlPatches) } -func additionalPatches(input string) string { - graphQLPatches := []Patch{ - { - Name: "GraphQL definitions (<=1.2.30)", - Regex: `((?:\w+ ?)?[\w$]+=)(\{kind:"Document",definitions:\[\{(?:\w+:[\w"]+,)+name:\{(?:\w+:[\w"]+,?)+value:("\w+"))`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify.GraphQL.Definitions[%s]=%s", submatches[1], submatches[3], submatches[2]) - }, - }, - { - Name: "GraphQL definitions (>=1.2.31)", - Regex: `(=new [\w_\$][\w_\$\d]*\.[\w_\$][\w_\$\d]*\("(\w+)","(query|mutation)","[\w\d]{64}",null\))`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf(`=Spicetify.GraphQL.Definitions["%s"]%s`, submatches[2], submatches[1]) - }, - }, - } +var graphQLPatches = []Patch{ + { + Name: "GraphQL definitions (<=1.2.30)", + Regex: `((?:\w+ ?)?[\w$]+=)(\{kind:"Document",definitions:\[\{(?:\w+:[\w"]+,)+name:\{(?:\w+:[\w"]+,?)+value:("\w+"))`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.GraphQL.Definitions[%s]=%s", submatches[1], submatches[3], submatches[2]) + }, + }, + { + Name: "GraphQL definitions (>=1.2.31)", + Regex: `(=new [\w_\$][\w_\$\d]*\.[\w_\$][\w_\$\d]*\("(\w+)","(query|mutation)","[\w\d]{64}",null\))`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf(`=Spicetify.GraphQL.Definitions["%s"]%s`, submatches[2], submatches[1]) + }, + }, +} +func additionalPatches(input string) string { return applyPatches(input, graphQLPatches) } const contextMenuLegacyRegex = `("Menu".+?children:)([\w$][\w$\d]*)` +var cssMapBareKeySpaceRe = regexp.MustCompile(`\b[a-zA-Z0-9_]{16,21}[ \t]+:`) var contextMenuComponentPattern = regexp.MustCompile(`function\(\{((?:[^{}]|\{[^{}]*\})*)\},[\w$]+\)\{`) var contextMenuChildrenPattern = regexp.MustCompile(`(?:^|,)\s*children\s*:\s*([\w$]+)`) +var uriObjRe = regexp.MustCompile(`(?:class ([\w$_]+)\{constructor|([\w$_]+)=function\(\)\{function ?[\w$_]+)\([\w$.,={}]+\)\{[\w !?:=.,>&(){}[\];]*this\.hasBase62Id`) +var buildRegex = regexp.MustCompile(`(Master|Release|PR|Local) Build.+(?:cef_)?(\d+\.\d+\.\d+\+g[0-9a-f]+\+chromium-\d+\.\d+\.\d+\.\d+)`) func patchContextMenuV2(input string) string { insertionIndex := -1 @@ -958,128 +919,163 @@ func patchContextMenuV2(input string) string { return input } +var xpuiPatches = []Patch{ + { + Name: "showNotification", + Regex: `(?:\w+ |,)([\w$]+)=(\([\w$]+=[\w$]+\.dispatch)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf(`;globalThis.Spicetify.showNotification=(message,isError=false,msTimeout)=>%s({message,feedbackType:isError?"ERROR":"NOTICE",msTimeout});const %s=%s`, submatches[1], submatches[1], submatches[2]) + }, + }, + { + Name: "Remove list of exclusive shows", + Regex: `\["spotify:show.+?\]`, + Replacement: func(submatches ...string) string { + return "[]" + }, + }, + { + Name: "Remove Star Wars easter eggs", + Regex: `\w+\(\)\.createElement\(\w+,\{onChange:this\.handleSaberStateChange\}\),`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Remove data-testid", + Regex: `"data-testid":("user-widget-link")?`, + Replacement: func(submatches ...string) string { + if submatches[1] != "" { + return submatches[0] + } + return `"":` + }, + }, + { + Name: "Expose PlatformAPI", + Regex: `((?:setTitlebarHeight|registerFactory)[\w(){}<>:.,&$!=;""?!#%/\- ]+)(\{version:[a-zA-Z_\$][\w\$]*,)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify._platform=%s", submatches[1], submatches[2]) + }, + }, + { + Name: "Redux store", + Regex: `(,[\w$]+=)(([$\w,.:=;(){}]+\(\{session:[\w$]+,features:[\w$]+,seoExperiment:[\w$]+\}))`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.Platform.ReduxStore=%s", submatches[1], submatches[2]) + }, + }, + { + Name: "React Component: Platform Provider", + Regex: `(,[$\w]+=)((function\([\w$]{1}\)\{var [\w$]+=[\w$]+\.platform,[\w$]+=[\w$]+\.children,)|(\(\{platform:[\w$]+,children:[\w$]+\}\)=>\{))`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.ReactComponent.PlatformProvider=%s", submatches[1], submatches[2]) + }, + }, + { + Name: "Prevent breaking popupLyrics", + Regex: `document.pictureInPictureElement&&\(\w+.current=[!\w]+,document\.exitPictureInPicture\(\)\),\w+\.current=null`, + Replacement: func(submatches ...string) string { + return "" + }, + }, + { + Name: "Spotify Custom Snackbar Interfaces (<=1.2.37)", + Regex: `\b\w\s*\(\)\s*[^;,]*enqueueCustomSnackbar:\s*(\w)\s*[^;]*;`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.Snackbar.enqueueCustomSnackbar=%s;", submatches[0], submatches[1]) + }, + }, + { + Name: "Spotify Custom Snackbar Interfaces (>=1.2.38)", + Regex: `(=)[^=]*\(\)\.enqueueCustomSnackbar;`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("=Spicetify.Snackbar.enqueueCustomSnackbar%s;", submatches[0]) + }, + }, + { + Name: "Spotify Image Snackbar Interface", + Regex: `(=)(\(\({[^}]*,\s*imageSrc)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.Snackbar.enqueueImageSnackbar=%s", submatches[1], submatches[2]) + }, + }, + { + Name: "React Component: Navigation for navLinks", + Regex: `(;const [\w\d]+=)((?:\(0,[\w\d]+\.memo\))[\(\d,\w\.\){:}=]+\=[\d\w]+\.[\d\w]+\.getLocaleForURLPath\(\))`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%sSpicetify.ReactComponent.Navigation=%s", submatches[1], submatches[2]) + }, + Once: true, + }, +} + func exposeAPIs_main(input string) string { inputContextMenu := utils.FindFirstMatch(input, `.*(?:value:"contextmenu"|"[^"]*":"context-menu")`) if len(inputContextMenu) > 0 { croppedInput := inputContextMenu[0] - react := utils.FindLastMatch(croppedInput, `([a-zA-Z_\$][\w\$]*)\.useRef`)[1] - candicates := utils.FindLastMatch(croppedInput, `\(\{[^}]*menu:([a-zA-Z_\$][\w\$]*),[^}]*trigger:([a-zA-Z_\$][\w\$]*),[^}]*triggerRef:([a-zA-Z_\$][\w\$]*)`) - oldCandicates := utils.FindLastMatch(croppedInput, `([a-zA-Z_\$][\w\$]*)=[\w_$]+\.menu[^}]*,([a-zA-Z_\$][\w\$]*)=[\w_$]+\.trigger[^}]*,([a-zA-Z_\$][\w\$]*)=[\w_$]+\.triggerRef`) - var menu, trigger, target string - if len(oldCandicates) != 0 { - menu = oldCandicates[1] - trigger = oldCandicates[2] - target = oldCandicates[3] - } else if len(candicates) != 0 { - menu = candicates[1] - trigger = candicates[2] - target = candicates[3] - } else { - menu = "e.menu" - trigger = "e.trigger" - target = "e.triggerRef" - } - - utils.Replace(&input, `\(0,([\w_$]+)\.jsx\)\((?:[\w_$]+\.[\w_$]+,\{value:"contextmenu"[^}]+\}\)\}\)|"[\w-]+",\{[^}]+:"context-menu"[^}]+\}\))`, func(submatches ...string) string { - return fmt.Sprintf("(0,%s.jsx)((Spicetify.ContextMenuV2._context||(Spicetify.ContextMenuV2._context=%s.createContext(null))).Provider,{value:{props:%s?.props,trigger:%s,target:%s},children:%s})", submatches[1], react, menu, trigger, target, submatches[0]) - }) - } + reactMatch := utils.FindLastMatch(croppedInput, `([a-zA-Z_\$][\w\$]*)\.useRef`) + if len(reactMatch) > 1 { + react := reactMatch[1] + candicates := utils.FindLastMatch(croppedInput, `\(\{[^}]*menu:([a-zA-Z_\$][\w\$]*),[^}]*trigger:([a-zA-Z_\$][\w\$]*),[^}]*triggerRef:([a-zA-Z_\$][\w\$]*)`) + oldCandicates := utils.FindLastMatch(croppedInput, `([a-zA-Z_\$][\w\$]*)=[\w_$]+\.menu[^}]*,([a-zA-Z_\$][\w\$]*)=[\w_$]+\.trigger[^}]*,([a-zA-Z_\$][\w\$]*)=[\w_$]+\.triggerRef`) + var menu, trigger, target string + if len(oldCandicates) != 0 { + menu = oldCandicates[1] + trigger = oldCandicates[2] + target = oldCandicates[3] + } else if len(candicates) != 0 { + menu = candicates[1] + trigger = candicates[2] + target = candicates[3] + } else { + menu = "e.menu" + trigger = "e.trigger" + target = "e.triggerRef" + } - xpuiPatches := []Patch{ - { - Name: "showNotification", - Regex: `(?:\w+ |,)([\w$]+)=(\([\w$]+=[\w$]+\.dispatch)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf(`;globalThis.Spicetify.showNotification=(message,isError=false,msTimeout)=>%s({message,feedbackType:isError?"ERROR":"NOTICE",msTimeout});const %s=%s`, submatches[1], submatches[1], submatches[2]) - }, - }, - { - Name: "Remove list of exclusive shows", - Regex: `\["spotify:show.+?\]`, - Replacement: func(submatches ...string) string { - return "[]" - }, - }, - { - Name: "Remove Star Wars easter eggs", - Regex: `\w+\(\)\.createElement\(\w+,\{onChange:this\.handleSaberStateChange\}\),`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Remove data-testid", - Regex: `"data-testid":("user-widget-link")?`, - Replacement: func(submatches ...string) string { - if submatches[1] != "" { - return submatches[0] - } - return `"":` - }, - }, - { - Name: "Expose PlatformAPI", - Regex: `((?:setTitlebarHeight|registerFactory)[\w(){}<>:.,&$!=;""?!#%/\- ]+)(\{version:[a-zA-Z_\$][\w\$]*,)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify._platform=%s", submatches[1], submatches[2]) - }, - }, - { - Name: "Redux store", - Regex: `(,[\w$]+=)(([$\w,.:=;(){}]+\(\{session:[\w$]+,features:[\w$]+,seoExperiment:[\w$]+\}))`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify.Platform.ReduxStore=%s", submatches[1], submatches[2]) - }, - }, - { - Name: "React Component: Platform Provider", - Regex: `(,[$\w]+=)((function\([\w$]{1}\)\{var [\w$]+=[\w$]+\.platform,[\w$]+=[\w$]+\.children,)|(\(\{platform:[\w$]+,children:[\w$]+\}\)=>\{))`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify.ReactComponent.PlatformProvider=%s", submatches[1], submatches[2]) - }, - }, - { - Name: "Prevent breaking popupLyrics", - Regex: `document.pictureInPictureElement&&\(\w+.current=[!\w]+,document\.exitPictureInPicture\(\)\),\w+\.current=null`, - Replacement: func(submatches ...string) string { - return "" - }, - }, - { - Name: "Spotify Custom Snackbar Interfaces (<=1.2.37)", - Regex: `\b\w\s*\(\)\s*[^;,]*enqueueCustomSnackbar:\s*(\w)\s*[^;]*;`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify.Snackbar.enqueueCustomSnackbar=%s;", submatches[0], submatches[1]) - }, - }, - { - Name: "Spotify Custom Snackbar Interfaces (>=1.2.38)", - Regex: `(=)[^=]*\(\)\.enqueueCustomSnackbar;`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("=Spicetify.Snackbar.enqueueCustomSnackbar%s;", submatches[0]) - }, - }, - { - Name: "Spotify Image Snackbar Interface", - Regex: `(=)(\(\({[^}]*,\s*imageSrc)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify.Snackbar.enqueueImageSnackbar=%s", submatches[1], submatches[2]) - }, - }, - { - Name: "React Component: Navigation for navLinks", - Regex: `(;const [\w\d]+=)((?:\(0,[\w\d]+\.memo\))[\(\d,\w\.\){:}=]+\=[\d\w]+\.[\d\w]+\.getLocaleForURLPath\(\))`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%sSpicetify.ReactComponent.Navigation=%s", submatches[1], submatches[2]) - }, - Once: true, - }, + utils.Replace(&input, `\(0,([\w_$]+)\.jsx\)\((?:[\w_$]+\.[\w_$]+,\{value:"contextmenu"[^}]+\}\)\}\)|"[\w-]+",\{[^}]+:"context-menu"[^}]+\}\))`, func(submatches ...string) string { + return fmt.Sprintf("(0,%s.jsx)((Spicetify.ContextMenuV2._context||(Spicetify.ContextMenuV2._context=%s.createContext(null))).Provider,{value:{props:%s?.props,trigger:%s,target:%s},children:%s})", submatches[1], react, menu, trigger, target, submatches[0]) + }) + } } input = applyPatches(input, xpuiPatches) return patchContextMenuV2(input) } +var vendorPatches = []Patch{ + { + Name: "Map styled-components classes", + Regex: `(\w+ [\w$_]+)=[\w$_]+\([\w$_]+>>>0\)`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%s=Spicetify._getStyledClassName(arguments,this)", submatches[1]) + }, + }, + { + Name: "Tippy.js", + Regex: `([\w\$_]+)\.setDefaultProps=`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("Spicetify.Tippy=%s;%s", submatches[1], submatches[0]) + }, + }, + { + Name: "Flipper components", + Regex: `([\w$]+)=((?:function|\()([\w$.,{}()= ]+(?:springConfig|overshootClamping)){2})`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("%s=Spicetify.ReactFlipToolkit.spring=%s", submatches[1], submatches[2]) + }, + }, + { + // https://github.com/iamhosseindhv/notistack + Name: "Snackbar", + Regex: `\w+\s*=\s*\w\.call\(this,[^)]+\)\s*\|\|\s*this\)\.enqueueSnackbar`, + Replacement: func(submatches ...string) string { + return fmt.Sprintf("Spicetify.Snackbar=%s", submatches[0]) + }, + }, +} + func exposeAPIs_vendor(input string) string { // URI utils.Replace( @@ -1088,48 +1084,10 @@ func exposeAPIs_vendor(input string) string { func(submatches ...string) string { return fmt.Sprintf(`,(globalThis.Spicetify.URI=%s)%s`, submatches[1], submatches[0]) }) - vendorPatches := []Patch{ - { - Name: "Spicetify.URI", - Regex: `,(\w+)\.prototype\.toAppType`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf(`,(globalThis.Spicetify.URI=%s)%s`, submatches[1], submatches[0]) - }, - }, - { - Name: "Map styled-components classes", - Regex: `(\w+ [\w$_]+)=[\w$_]+\([\w$_]+>>>0\)`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%s=Spicetify._getStyledClassName(arguments,this)", submatches[1]) - }, - }, - { - Name: "Tippy.js", - Regex: `([\w\$_]+)\.setDefaultProps=`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("Spicetify.Tippy=%s;%s", submatches[1], submatches[0]) - }, - }, - { - Name: "Flipper components", - Regex: `([\w$]+)=((?:function|\()([\w$.,{}()= ]+(?:springConfig|overshootClamping)){2})`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("%s=Spicetify.ReactFlipToolkit.spring=%s", submatches[1], submatches[2]) - }, - }, - { - // https://github.com/iamhosseindhv/notistack - Name: "Snackbar", - Regex: `\w+\s*=\s*\w\.call\(this,[^)]+\)\s*\|\|\s*this\)\.enqueueSnackbar`, - Replacement: func(submatches ...string) string { - return fmt.Sprintf("Spicetify.Snackbar=%s", submatches[0]) - }, - }, - } // URI after 1.2.4 if !strings.Contains(input, "Spicetify.URI") { - URIObj := regexp.MustCompile(`(?:class ([\w$_]+)\{constructor|([\w$_]+)=function\(\)\{function ?[\w$_]+)\([\w$.,={}]+\)\{[\w !?:=.,>&(){}[\];]*this\.hasBase62Id`).FindStringSubmatch(input) + URIObj := uriObjRe.FindStringSubmatch(input) if len(URIObj) != 0 { URI := utils.SeekToCloseParen( @@ -1137,17 +1095,19 @@ func exposeAPIs_vendor(input string) string { `\{(?:constructor|function ?[\w$_]+)\([\w$.,={}]+\)\{[\w !?:=.,>&(){}[\];]*this\.hasBase62Id`, '{', '}') - if URIObj[1] == "" { - URIObj[1] = URIObj[2] - // Class is a self-invoking function - URI = fmt.Sprintf("%s()", URI) - } + if URI != "" { + if URIObj[1] == "" { + URIObj[1] = URIObj[2] + // Class is a self-invoking function + URI = fmt.Sprintf("%s()", URI) + } - input = strings.Replace( - input, - URI, - fmt.Sprintf("%s;Spicetify.URI=%s;", URI, URIObj[1]), - 1) + input = strings.Replace( + input, + URI, + fmt.Sprintf("%s;Spicetify.URI=%s;", URI, URIObj[1]), + 1) + } } } @@ -1160,7 +1120,6 @@ func validateReleaseBuild(spotifyBinaryPath string) error { return fmt.Errorf("could not read %s: %w", filepath.Base(spotifyBinaryPath), err) } - buildRegex := regexp.MustCompile(`(Master|Release|PR|Local) Build.+(?:cef_)?(\d+\.\d+\.\d+\+g[0-9a-f]+\+chromium-\d+\.\d+\.\d+\.\d+)`) matches := buildRegex.FindSubmatch(fileContent) if len(matches) == 0 { diff --git a/src/utils/utils.go b/src/utils/utils.go index f3cd6c220f..9614eb5a44 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -12,11 +12,33 @@ import ( "regexp" "runtime" "strings" + "sync" "time" "github.com/go-ini/ini" ) +var regexpCache sync.Map + +func getRegexp(pattern string) *regexp.Regexp { + if cached, ok := regexpCache.Load(pattern); ok { + return cached.(*regexp.Regexp) + } + re := regexp.MustCompile(pattern) + actual, _ := regexpCache.LoadOrStore(pattern, re) + return actual.(*regexp.Regexp) +} + +func extractSubmatches(input string, loc []int) []string { + submatches := make([]string, len(loc)/2) + for i := 0; i < len(loc); i += 2 { + if loc[i] >= 0 && loc[i+1] >= 0 { + submatches[i/2] = input[loc[i]:loc[i+1]] + } + } + return submatches +} + // CheckExistAndCreate checks folder existence // and makes that folder, recursively, if it does not exist func CheckExistAndCreate(dir string) { @@ -44,12 +66,6 @@ func Unzip(src, dest string) error { defer r.Close() for _, f := range r.File { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - fpath := filepath.Join(dest, f.Name) if f.FileInfo().IsDir() { os.MkdirAll(fpath, 0700) @@ -64,14 +80,22 @@ func Unzip(src, dest string) error { log.Fatal(err) return err } - f, err := os.OpenFile( + + rc, err := f.Open() + if err != nil { + return err + } + + outFile, err := os.OpenFile( fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) if err != nil { + rc.Close() return err } - defer f.Close() - _, err = io.Copy(f, rc) + _, err = io.Copy(outFile, rc) + outFile.Close() + rc.Close() if err != nil { return err } @@ -115,21 +139,7 @@ func Copy(src, dest string, recursive bool, filters []string) error { } } - fSrc, err := os.Open(fSrcPath) - if err != nil { - return err - } - defer fSrc.Close() - - fDest, err := os.OpenFile( - fDestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) - if err != nil { - return err - } - defer fDest.Close() - - _, err = io.Copy(fDest, fSrc) - if err != nil { + if err := CopyFile(fSrcPath, dest); err != nil { return err } } @@ -165,66 +175,65 @@ func CopyFile(srcPath, dest string) error { // Replace uses Regexp to find any matched from `input` with `regexpTerm` // and replaces them with `replaceTerm` then returns new string. func Replace(str *string, pattern string, repl func(submatches ...string) string) { - re := regexp.MustCompile(pattern) - *str = re.ReplaceAllStringFunc(*str, func(match string) string { - submatches := re.FindStringSubmatch(match) - return repl(submatches...) - }) + re := getRegexp(pattern) + input := *str + matches := re.FindAllStringSubmatchIndex(input, -1) + if len(matches) == 0 { + return + } + + var b strings.Builder + b.Grow(len(input)) + lastIndex := 0 + + for _, loc := range matches { + b.WriteString(input[lastIndex:loc[0]]) + b.WriteString(repl(extractSubmatches(input, loc)...)) + lastIndex = loc[1] + } + b.WriteString(input[lastIndex:]) + *str = b.String() } func ReplaceOnce(str *string, pattern string, repl func(submatches ...string) string) { - re := regexp.MustCompile(pattern) - firstMatch := true - *str = re.ReplaceAllStringFunc(*str, func(match string) string { - if firstMatch { - firstMatch = false - submatches := re.FindStringSubmatch(match) - if submatches != nil { - return repl(submatches...) - } - } - return match - }) + re := getRegexp(pattern) + input := *str + loc := re.FindStringSubmatchIndex(input) + if loc == nil { + return + } + + *str = input[:loc[0]] + repl(extractSubmatches(input, loc)...) + input[loc[1]:] } func ReplaceOnceWithPriority(str *string, patterns []string, repl func(index int, submatches ...string) string) { + input := *str for i, pattern := range patterns { - re := regexp.MustCompile(pattern) - firstMatch := true - *str = re.ReplaceAllStringFunc(*str, func(match string) string { - if firstMatch { - firstMatch = false - submatches := re.FindStringSubmatch(match) - if submatches != nil { - return repl(i, submatches...) - } - } - return match - }) - if !firstMatch { + re := getRegexp(pattern) + loc := re.FindStringSubmatchIndex(input) + if loc != nil { + *str = input[:loc[0]] + repl(i, extractSubmatches(input, loc)...) + input[loc[1]:] break } } } func FindMatch(input string, regexpTerm string) [][]string { - re := regexp.MustCompile(regexpTerm) + re := getRegexp(regexpTerm) matches := re.FindAllStringSubmatch(input, -1) return matches } func FindFirstMatch(input string, regexpTerm string) []string { - matches := FindMatch(input, regexpTerm) - if len(matches) > 0 { - return matches[0] - } - return nil + re := getRegexp(regexpTerm) + return re.FindStringSubmatch(input) } func FindLastMatch(input string, regexpTerm string) []string { - matches := FindMatch(input, regexpTerm) + re := getRegexp(regexpTerm) + matches := re.FindAllStringSubmatchIndex(input, -1) if len(matches) > 0 { - return matches[len(matches)-1] + return extractSubmatches(input, matches[len(matches)-1]) } return nil } @@ -234,14 +243,18 @@ func FindLastMatch(input string, regexpTerm string) []string { func ModifyFile(path string, repl func(string) string) { raw, err := os.ReadFile(path) if err != nil { - log.Print(err) + PrintError(err.Error()) return } - content := repl(string(raw)) + original := string(raw) + content := repl(original) + if content == original { + return + } - if err := os.WriteFile(path, []byte(content), 0700); err != nil { - log.Print(err) + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + PrintError(err.Error()) } } @@ -301,7 +314,7 @@ func PrependTime(text string) string { // function symbol in obfuscated code. func FindSymbol(debugInfo, content string, clues []string) []string { for _, v := range clues { - re := regexp.MustCompile(v) + re := getRegexp(v) found := re.FindStringSubmatch(content) if found != nil { return found[1:] @@ -319,7 +332,7 @@ func FindSymbol(debugInfo, content string, clues []string) []string { // function symbol in obfuscated code. Returns the matched symbols and the pattern that matched. func FindSymbolWithPattern(debugInfo, content string, clues []string) ([]string, string) { for _, v := range clues { - re := regexp.MustCompile(v) + re := getRegexp(v) found := re.FindStringSubmatch(content) if found != nil { return found[1:], v @@ -348,14 +361,14 @@ func CreateJunction(location, destination string) error { } func SeekToCloseParen(content string, regexpTerm string, leftChar, rightChar byte) string { - loc := regexp.MustCompile(regexpTerm).FindStringIndex(content) + loc := getRegexp(regexpTerm).FindStringIndex(content) if len(loc) > 0 { start := loc[0] end := start count := 0 init := false - for { + for end < len(content) { switch content[end] { case leftChar: count += 1 @@ -365,10 +378,9 @@ func SeekToCloseParen(content string, regexpTerm string, leftChar, rightChar byt } end += 1 if count == 0 && init { - break + return content[start:end] } } - return content[start:end] } return "" }