playworks
This commit is contained in:
BIN
dist/hpl-toolbox-0.1.6.exe
vendored
BIN
dist/hpl-toolbox-0.1.6.exe
vendored
Binary file not shown.
BIN
dist/hpl-toolbox-0.1.6.vsix
vendored
BIN
dist/hpl-toolbox-0.1.6.vsix
vendored
Binary file not shown.
@@ -95,10 +95,11 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']);
|
||||
const SUPPORTED_NETWORKS = new Set(['al', 'is', 'fb', 'gg', 'mo', 'vu', 'mtg', 'tt']);
|
||||
const MRAID_NETWORKS = new Set(['al', 'is']);
|
||||
const NETWORK_OUTPUT_FOLDERS: Record<string, string> = {
|
||||
al: 'Applovin',
|
||||
is: 'Ironsource',
|
||||
un: 'Unity',
|
||||
fb: 'Facebook',
|
||||
gg: 'GoogleAds',
|
||||
mo: 'Moloco',
|
||||
@@ -120,6 +121,9 @@ function outputBaseName(opts: ConvertOptions): string {
|
||||
}
|
||||
|
||||
async function convertNetwork(html: string, opts: ConvertOptions, network: string): Promise<string> {
|
||||
if (!SUPPORTED_NETWORKS.has(network)) {
|
||||
throw new Error(`Unsupported Playworks target network: ${network}`);
|
||||
}
|
||||
const transformed = transformHtml(html, network);
|
||||
const needsZip = ZIPPED_NETWORKS.has(network);
|
||||
const baseName = outputBaseName(opts);
|
||||
@@ -150,34 +154,68 @@ function transformHtml(html: string, network: string): string {
|
||||
// Inject into </head>: lifecycle stubs only (must register luna:build listener
|
||||
// BEFORE the Playworks bundle so our handler fires first), then any network SDK script.
|
||||
// Console restore stays at end-of-body — it must run AFTER the bundle overrides console.
|
||||
result = removeNetworkSdkScripts(result, network);
|
||||
let headInject = buildLifecycleScript();
|
||||
if (network === 'al' || network === 'is') {
|
||||
if (MRAID_NETWORKS.has(network)) {
|
||||
headInject += '<script src="mraid.js"></script>';
|
||||
} else if (network === 'gg') {
|
||||
headInject += '<script src="exitapi.js"></script>';
|
||||
}
|
||||
result = result.replace('</head>', headInject + '\n</head>');
|
||||
result = injectBeforeHeadClose(result, headInject);
|
||||
|
||||
// Body-level flags
|
||||
if (network === 'vu') {
|
||||
result = result.replace('<body>', '<body>\n<script>window.__VUNGLE__=true;</script>');
|
||||
result = injectAfterBodyOpen(result, '<script>window.__VUNGLE__=true;</script>');
|
||||
} else if (network === 'mtg') {
|
||||
result = result.replace('<body>', '<body onload="gameReady()">');
|
||||
result = addBodyOnload(result, 'gameReady()');
|
||||
} else if (network === 'tt') {
|
||||
result = result.replace('<body>', '<body>\n<script>window.__TIKTOK__=true;</script>');
|
||||
result = injectAfterBodyOpen(result, '<script>window.__TIKTOK__=true;</script>');
|
||||
}
|
||||
|
||||
// Replace the CTA script
|
||||
result = replaceCTAScript(result, network);
|
||||
|
||||
// Unity: rewrite window.top → window.self (Luna static scan requirement)
|
||||
if (network === 'un') {
|
||||
result = result.split('window.top').join('window.self');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function removeNetworkSdkScripts(html: string, network: string): string {
|
||||
let result = html;
|
||||
if (MRAID_NETWORKS.has(network)) {
|
||||
result = result.replace(/<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*<\/script>\s*/gi, '');
|
||||
}
|
||||
if (network === 'gg') {
|
||||
result = result.replace(/<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*<\/script>\s*/gi, '');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function injectBeforeHeadClose(html: string, injection: string): string {
|
||||
if (/<\/head>/i.test(html)) {
|
||||
return html.replace(/<\/head>/i, `${injection}\n</head>`);
|
||||
}
|
||||
return `${injection}\n${html}`;
|
||||
}
|
||||
|
||||
function injectAfterBodyOpen(html: string, injection: string): string {
|
||||
if (/<body\b[^>]*>/i.test(html)) {
|
||||
return html.replace(/<body\b[^>]*>/i, match => `${match}\n${injection}`);
|
||||
}
|
||||
return `${injection}\n${html}`;
|
||||
}
|
||||
|
||||
function addBodyOnload(html: string, handler: string): string {
|
||||
if (!/<body\b[^>]*>/i.test(html)) {
|
||||
return `<body onload="${handler}">\n${html}`;
|
||||
}
|
||||
return html.replace(/<body\b([^>]*)>/i, (match, attrs: string) => {
|
||||
const onloadMatch = attrs.match(/\bonload\s*=\s*(["'])(.*?)\1/i);
|
||||
if (!onloadMatch) return `<body${attrs} onload="${handler}">`;
|
||||
if (onloadMatch[2].includes(handler)) return match;
|
||||
const updated = `${onloadMatch[2]};${handler}`;
|
||||
return match.replace(onloadMatch[0], `onload=${onloadMatch[1]}${updated}${onloadMatch[1]}`);
|
||||
});
|
||||
}
|
||||
|
||||
function replaceCTAScript(html: string, network: string): string {
|
||||
const marker = 'Luna.Unity.Playable.InstallFullGame=function';
|
||||
const markerIdx = html.lastIndexOf(marker);
|
||||
@@ -245,11 +283,7 @@ function buildCTAScript(network: string): string {
|
||||
case 'al':
|
||||
case 'is':
|
||||
// MRAID — mraid.js injected in <head>
|
||||
ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}`;
|
||||
break;
|
||||
case 'un':
|
||||
// Unity Ads — MRAID-based; window.top already rewritten separately
|
||||
ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}`;
|
||||
ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`;
|
||||
break;
|
||||
case 'fb':
|
||||
ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`;
|
||||
@@ -259,7 +293,7 @@ function buildCTAScript(network: string): string {
|
||||
ctaLogic = `${closeCall}if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`;
|
||||
break;
|
||||
case 'mo':
|
||||
ctaLogic = `${closeCall}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`;
|
||||
ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`;
|
||||
break;
|
||||
case 'vu':
|
||||
// Vungle — window.__VUNGLE__ flag set in body
|
||||
@@ -314,10 +348,9 @@ function getHtml(): string {
|
||||
const networks = [
|
||||
{ tag: 'al', label: 'Applovin', note: 'HTML + mraid.js in head' },
|
||||
{ tag: 'is', label: 'Ironsource', note: 'HTML + mraid.js in head' },
|
||||
{ tag: 'un', label: 'Unity', note: 'HTML (window.top → self)' },
|
||||
{ tag: 'fb', label: 'Facebook', note: 'HTML' },
|
||||
{ tag: 'gg', label: 'Google Ads', note: 'ZIP + exitapi.js in head' },
|
||||
{ tag: 'mo', label: 'Moloco', note: 'HTML' },
|
||||
{ tag: 'mo', label: 'Moloco', note: 'HTML + FbPlayableAd CTA' },
|
||||
{ tag: 'vu', label: 'Vungle', note: 'ZIP + __VUNGLE__ flag' },
|
||||
{ tag: 'mtg', label: 'Mintegral', note: 'ZIP + onload="gameReady()"' },
|
||||
{ tag: 'tt', label: 'TikTok', note: 'HTML + __TIKTOK__ flag' },
|
||||
|
||||
@@ -77,10 +77,9 @@ func PlayworksPage(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="net-grid">
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="al" checked /><span class="net-label">Applovin <span class="net-tag">al</span></span><span class="net-note">HTML + mraid.js in head</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="is" checked /><span class="net-label">Ironsource <span class="net-tag">is</span></span><span class="net-note">HTML + mraid.js in head</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="un" checked /><span class="net-label">Unity <span class="net-tag">un</span></span><span class="net-note">HTML (window.top to self)</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="fb" checked /><span class="net-label">Facebook <span class="net-tag">fb</span></span><span class="net-note">HTML</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="gg" checked /><span class="net-label">Google Ads <span class="net-tag">gg</span></span><span class="net-note">ZIP + exitapi.js in head</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML + FbPlayableAd CTA</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label>
|
||||
@@ -230,6 +229,11 @@ func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`)
|
||||
var playworksMraidScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*</script>\s*`)
|
||||
var playworksExitapiScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*</script>\s*`)
|
||||
var playworksHeadCloseRx = regexp.MustCompile(`(?i)</head>`)
|
||||
var playworksBodyOpenRx = regexp.MustCompile(`(?i)<body\b[^>]*>`)
|
||||
var playworksBodyTagRx = regexp.MustCompile(`(?i)<body\b([^>]*)>`)
|
||||
|
||||
func playworksSourceBaseName(sourcePath string) string {
|
||||
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
|
||||
@@ -259,6 +263,9 @@ func playworksOutputBaseName(opts playworksConvertOptions) string {
|
||||
}
|
||||
|
||||
func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) {
|
||||
if !playworksIsSupportedNetwork(network) {
|
||||
return "", fmt.Errorf("unsupported Playworks target network: %s", network)
|
||||
}
|
||||
transformed := transformPlayworksHTML(htmlSrc, network)
|
||||
baseName := playworksOutputBaseName(opts)
|
||||
htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network)
|
||||
@@ -288,8 +295,6 @@ func playworksNetworkOutputFolder(network string) string {
|
||||
return "Applovin"
|
||||
case "is":
|
||||
return "Ironsource"
|
||||
case "un":
|
||||
return "Unity"
|
||||
case "fb":
|
||||
return "Facebook"
|
||||
case "gg":
|
||||
@@ -311,33 +316,130 @@ func playworksNeedsZip(network string) bool {
|
||||
return network == "gg" || network == "vu" || network == "mtg"
|
||||
}
|
||||
|
||||
func playworksIsMraidNetwork(network string) bool {
|
||||
return network == "al" || network == "is"
|
||||
}
|
||||
|
||||
func playworksIsSupportedNetwork(network string) bool {
|
||||
switch network {
|
||||
case "al", "is", "fb", "gg", "mo", "vu", "mtg", "tt":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func transformPlayworksHTML(htmlSrc, network string) string {
|
||||
result := htmlSrc
|
||||
result = removePlayworksNetworkScripts(result, network)
|
||||
headInject := buildPlayworksLifecycleScript()
|
||||
if network == "al" || network == "is" {
|
||||
if playworksIsMraidNetwork(network) {
|
||||
headInject += `<script src="mraid.js"></script>`
|
||||
} else if network == "gg" {
|
||||
headInject += `<script src="exitapi.js"></script>`
|
||||
}
|
||||
result = strings.Replace(result, "</head>", headInject+"\n</head>", 1)
|
||||
result = injectPlayworksBeforeHeadClose(result, headInject)
|
||||
|
||||
if network == "vu" {
|
||||
result = strings.Replace(result, "<body>", `<body>
|
||||
<script>window.__VUNGLE__=true;</script>`, 1)
|
||||
result = injectPlayworksAfterBodyOpen(result, `<script>window.__VUNGLE__=true;</script>`)
|
||||
} else if network == "mtg" {
|
||||
result = strings.Replace(result, "<body>", `<body onload="gameReady()">`, 1)
|
||||
result = addPlayworksBodyOnload(result, "gameReady()")
|
||||
} else if network == "tt" {
|
||||
result = strings.Replace(result, "<body>", `<body>
|
||||
<script>window.__TIKTOK__=true;</script>`, 1)
|
||||
result = injectPlayworksAfterBodyOpen(result, `<script>window.__TIKTOK__=true;</script>`)
|
||||
}
|
||||
|
||||
result = replacePlayworksCTAScript(result, network)
|
||||
if network == "un" {
|
||||
result = strings.ReplaceAll(result, "window.top", "window.self")
|
||||
return result
|
||||
}
|
||||
|
||||
func removePlayworksNetworkScripts(htmlSrc, network string) string {
|
||||
result := htmlSrc
|
||||
if playworksIsMraidNetwork(network) {
|
||||
result = playworksMraidScriptRx.ReplaceAllString(result, "")
|
||||
}
|
||||
if network == "gg" {
|
||||
result = playworksExitapiScriptRx.ReplaceAllString(result, "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func injectPlayworksBeforeHeadClose(htmlSrc, injection string) string {
|
||||
if playworksHeadCloseRx.MatchString(htmlSrc) {
|
||||
return playworksHeadCloseRx.ReplaceAllString(htmlSrc, injection+"\n</head>")
|
||||
}
|
||||
return injection + "\n" + htmlSrc
|
||||
}
|
||||
|
||||
func injectPlayworksAfterBodyOpen(htmlSrc, injection string) string {
|
||||
loc := playworksBodyOpenRx.FindStringIndex(htmlSrc)
|
||||
if loc == nil {
|
||||
return injection + "\n" + htmlSrc
|
||||
}
|
||||
return htmlSrc[:loc[1]] + "\n" + injection + htmlSrc[loc[1]:]
|
||||
}
|
||||
|
||||
func addPlayworksBodyOnload(htmlSrc, handler string) string {
|
||||
loc := playworksBodyTagRx.FindStringSubmatchIndex(htmlSrc)
|
||||
if loc == nil {
|
||||
return `<body onload="` + handler + `">` + "\n" + htmlSrc
|
||||
}
|
||||
attrs := htmlSrc[loc[2]:loc[3]]
|
||||
onloadStart, onloadEnd, quote, valueStart, valueEnd := findPlayworksOnloadAttr(attrs)
|
||||
if onloadStart == -1 {
|
||||
updated := `<body` + attrs + ` onload="` + handler + `">`
|
||||
return htmlSrc[:loc[0]] + updated + htmlSrc[loc[1]:]
|
||||
}
|
||||
value := attrs[valueStart:valueEnd]
|
||||
if strings.Contains(value, handler) {
|
||||
return htmlSrc
|
||||
}
|
||||
replacement := `onload=` + quote + value + `;` + handler + quote
|
||||
updatedAttrs := attrs[:onloadStart] + replacement + attrs[onloadEnd:]
|
||||
updatedTag := `<body` + updatedAttrs + `>`
|
||||
return htmlSrc[:loc[0]] + updatedTag + htmlSrc[loc[1]:]
|
||||
}
|
||||
|
||||
func findPlayworksOnloadAttr(attrs string) (start int, end int, quote string, valueStart int, valueEnd int) {
|
||||
lower := strings.ToLower(attrs)
|
||||
idx := strings.Index(lower, "onload")
|
||||
for idx != -1 {
|
||||
if idx > 0 {
|
||||
prev := lower[idx-1]
|
||||
if (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') || prev == '-' || prev == '_' {
|
||||
next := strings.Index(lower[idx+len("onload"):], "onload")
|
||||
if next == -1 {
|
||||
return -1, -1, "", -1, -1
|
||||
}
|
||||
idx += len("onload") + next
|
||||
continue
|
||||
}
|
||||
}
|
||||
pos := idx + len("onload")
|
||||
for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
|
||||
pos++
|
||||
}
|
||||
if pos >= len(attrs) || attrs[pos] != '=' {
|
||||
return -1, -1, "", -1, -1
|
||||
}
|
||||
pos++
|
||||
for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
|
||||
pos++
|
||||
}
|
||||
if pos >= len(attrs) || (attrs[pos] != '"' && attrs[pos] != '\'') {
|
||||
return -1, -1, "", -1, -1
|
||||
}
|
||||
q := attrs[pos]
|
||||
valueStart = pos + 1
|
||||
valueEnd = strings.IndexByte(attrs[valueStart:], q)
|
||||
if valueEnd == -1 {
|
||||
return -1, -1, "", -1, -1
|
||||
}
|
||||
valueEnd += valueStart
|
||||
return idx, valueEnd + 1, string(q), valueStart, valueEnd
|
||||
}
|
||||
return -1, -1, "", -1, -1
|
||||
}
|
||||
|
||||
func replacePlayworksCTAScript(htmlSrc, network string) string {
|
||||
marker := "Luna.Unity.Playable.InstallFullGame=function"
|
||||
markerIdx := strings.LastIndex(htmlSrc, marker)
|
||||
@@ -393,15 +495,13 @@ func buildPlayworksCTAScript(network string) string {
|
||||
ctaLogic := closeCall + `window.open(o,"_blank");`
|
||||
switch network {
|
||||
case "al", "is":
|
||||
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}`
|
||||
case "un":
|
||||
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}`
|
||||
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`
|
||||
case "fb":
|
||||
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`
|
||||
case "gg":
|
||||
ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`
|
||||
case "mo":
|
||||
ctaLogic = closeCall + `if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
|
||||
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
|
||||
case "vu":
|
||||
ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}`
|
||||
case "mtg":
|
||||
|
||||
Reference in New Issue
Block a user