package web import ( "io/fs" "path/filepath" "strings" "testing" ) // TestInlineScriptHasNoMultilineStrings 守住「模板里的内联 JS 不许出现跨行字符串」。 // // JS 的单引号字符串不能含裸换行;真出现一个,那一整块 之间的内容 } // scriptBlocks 抠出每个 ") if end < 0 { break } start := line out = append(out, scriptBlock{line: start, text: rest[:end]}) line += strings.Count(rest[:end], "\n") + strings.Count("", "\n") + 1 s = rest[end:] } return out } // unterminatedString 找「单/双引号字符串里出现裸换行」的第一处。 // 跳过 // 行注释、/* */ 块注释、反引号模板串,以及 Go 模板动作 {{…}} // (动作里可能带引号,不该被当成 JS 字符串的边界)。 func unterminatedString(js string) (scriptBlock, bool) { var ( quote byte // 当前所在字符串的引号,0 表示不在字符串里 tick bool // 反引号模板串 blockC bool // /* */ 里 action int // 处于 {{…}} 里的深度 line = 1 ) for i := 0; i < len(js); i++ { c := js[i] if c == '\n' { line++ if quote != 0 { from := strings.LastIndex(js[:i], "\n") + 1 return scriptBlock{line: line, text: js[from:i]}, true } continue } if blockC { if c == '*' && i+1 < len(js) && js[i+1] == '/' { blockC = false i++ } continue } if action > 0 { if c == '}' && i+1 < len(js) && js[i+1] == '}' { action = 0 i++ } continue } if tick { if c == '`' { tick = false } continue } if quote != 0 { if c == '\\' { i++ } else if c == quote { quote = 0 } continue } switch { case c == '{' && i+1 < len(js) && js[i+1] == '{': action++ i++ case c == '/' && i+1 < len(js) && js[i+1] == '/': nl := strings.IndexByte(js[i:], '\n') if nl < 0 { return scriptBlock{}, false } line += strings.Count(js[i:i+nl], "\n") i += nl case c == '/' && i+1 < len(js) && js[i+1] == '*': blockC = true i++ case c == '`': tick = true case c == '\'' || c == '"': quote = c } } return scriptBlock{}, false }