diff --git a/dist/hpl-toolbox-0.1.6.exe b/dist/hpl-toolbox-0.1.6.exe index b78687d..47293ee 100644 Binary files a/dist/hpl-toolbox-0.1.6.exe and b/dist/hpl-toolbox-0.1.6.exe differ diff --git a/dist/hpl-toolbox-0.1.6.vsix b/dist/hpl-toolbox-0.1.6.vsix index 5ffa0d0..417596c 100644 Binary files a/dist/hpl-toolbox-0.1.6.vsix and b/dist/hpl-toolbox-0.1.6.vsix differ diff --git a/src/tools/mraidChecker.ts b/src/tools/mraidChecker.ts index bd38a78..ee94f57 100644 --- a/src/tools/mraidChecker.ts +++ b/src/tools/mraidChecker.ts @@ -80,15 +80,21 @@ export function openMraidChecker(_context: vscode.ExtensionContext) { let targets: string[] = []; let baseDir = ''; let skipped = 0; + let skippedReason = ''; + let noTargetsError = 'No HTML files were found.'; if (files && files.length) { const existing = files.filter(f => fs.existsSync(f)); - targets = existing.filter(isSupportedMraidBuild); - skipped = existing.length - targets.length; + targets = existing; + skipped = files.length - existing.length; + skippedReason = 'missing file(s).'; + noTargetsError = 'No selected HTML files were found.'; baseDir = targets.length ? commonBaseDir(targets) : ''; } else if (folder && fs.existsSync(folder)) { const allHtmlFiles = await collectHtmlFiles(folder); - targets = allHtmlFiles.filter(isSupportedMraidBuild); + targets = allHtmlFiles.filter(isInSupportedMraidFolderPath); skipped = allHtmlFiles.length - targets.length; + skippedReason = 'HTML file(s) outside AppLovin/ironSource/Unity folders.'; + noTargetsError = 'No AppLovin, ironSource, or Unity folder HTML files were found.'; baseDir = folder; } else { panel.webview.postMessage({ type: 'results', results: [], error: 'Pick a folder or files first' }); @@ -100,7 +106,8 @@ export function openMraidChecker(_context: vscode.ExtensionContext) { type: 'results', results: [], skipped, - error: 'No AppLovin, ironSource, or Unity HTML builds were found.', + skippedReason, + error: noTargetsError, }); return; } @@ -116,7 +123,7 @@ export function openMraidChecker(_context: vscode.ExtensionContext) { // Skip unreadable files, matching the scanner pattern used elsewhere. } } - panel.webview.postMessage({ type: 'results', results, skipped }); + panel.webview.postMessage({ type: 'results', results, skipped, skippedReason }); } }); } @@ -193,9 +200,24 @@ function collectMraidCallLines(html: string): Map { return calls; } -function isSupportedMraidBuild(filePath: string): boolean { - const normalized = filePath.toLowerCase().replace(/\\/g, '/'); - return /(^|[\/._ -])(al|applovin|app-lovin|is|iron.?source|un|unity|unityads|unity-ads)([\/._ -]|$)/i.test(normalized); +function isInSupportedMraidFolderPath(filePath: string): boolean { + const folderParts = path.dirname(filePath) + .split(/[\\/]+/) + .map(part => part.toLowerCase()); + return folderParts.some(part => [ + 'al', + 'applovin', + 'app-lovin', + 'is', + 'ironsource', + 'iron-source', + 'iron_source', + 'un', + 'unity', + 'unityads', + 'unity-ads', + 'unity_ads', + ].includes(part)); } function hasMraidReference(ctx: ScanContext): boolean { @@ -387,7 +409,7 @@ function getHtml(): string {

MRAID Checker

-
Static checks for AppLovin, ironSource, and Unity HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.
+
Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.
@@ -457,8 +479,9 @@ function getHtml(): string { const requirements = msg.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'requirement').length, 0); const bestPractices = msg.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'best-practice').length, 0); const skipped = Number(msg.skipped || 0); - const skippedText = skipped ? ' Skipped ' + skipped + ' non-target HTML file(s).' : ''; - statusEl.innerHTML = '
Scanned ' + total + ' AppLovin/ironSource/Unity HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '
'; + const skippedReason = msg.skippedReason || 'file(s).'; + const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : ''; + statusEl.innerHTML = '
Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '
'; resultsEl.innerHTML = ''; for (const r of msg.results) { const requirementsForFile = r.issues.filter(i => i.severity === 'requirement').length; diff --git a/src/tools/playworksConverter.ts b/src/tools/playworksConverter.ts index c7a0a4a..7a5af6e 100644 --- a/src/tools/playworksConverter.ts +++ b/src/tools/playworksConverter.ts @@ -94,6 +94,17 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) { } const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']); +const NETWORK_OUTPUT_FOLDERS: Record = { + al: 'Applovin', + is: 'Ironsource', + un: 'Unity', + fb: 'Facebook', + gg: 'GoogleAds', + mo: 'Moloco', + vu: 'Vungle', + mtg: 'Mintegral', + tt: 'TikTok', +}; function sourceBaseName(sourcePath: string): string { return path.basename(sourcePath, path.extname(sourcePath)) @@ -105,10 +116,12 @@ async function convertNetwork(html: string, opts: ConvertOptions, network: strin const needsZip = ZIPPED_NETWORKS.has(network); const baseName = sourceBaseName(opts.sourcePath); const htmlFileName = `${baseName}_${network}.html`; + const outputDir = path.join(opts.outputDir, NETWORK_OUTPUT_FOLDERS[network] ?? network); + fs.mkdirSync(outputDir, { recursive: true }); if (needsZip) { - const tmpPath = path.join(opts.outputDir, `_tmp_${Date.now()}_${network}.html`); - const zipPath = path.join(opts.outputDir, `${baseName}_${network}.zip`); + const tmpPath = path.join(outputDir, `_tmp_${Date.now()}_${network}.html`); + const zipPath = path.join(outputDir, `${baseName}_${network}.zip`); fs.writeFileSync(tmpPath, transformed, 'utf8'); try { await createZip(tmpPath, 'index.html', zipPath); @@ -118,7 +131,7 @@ async function convertNetwork(html: string, opts: ConvertOptions, network: strin return zipPath; } - const outPath = path.join(opts.outputDir, htmlFileName); + const outPath = path.join(outputDir, htmlFileName); fs.writeFileSync(outPath, transformed, 'utf8'); return outPath; } @@ -431,6 +444,11 @@ function getHtml(): string { if (outputDir) vscode.postMessage({ type: 'openOutput', dir: outputDir }); }); + function outputDisplayName(file) { + const parts = file.split(/[\\\\/]/).filter(Boolean); + return parts.slice(-2).join('/'); + } + document.getElementById('convert').addEventListener('click', () => { const networks = [...document.querySelectorAll('.net-cb')] .filter(c => c.checked).map(c => c.dataset.tag); @@ -484,7 +502,7 @@ function getHtml(): string { const detail = document.createElement('span'); if (r.ok) { detail.className = 'res-file'; - detail.textContent = r.file.split(/[\\\\/]/).pop(); + detail.textContent = outputDisplayName(r.file); } else { detail.className = 'res-err'; detail.textContent = r.error || 'Unknown error'; diff --git a/standalone/mraid.go b/standalone/mraid.go index 5d34f33..6739c4f 100644 --- a/standalone/mraid.go +++ b/standalone/mraid.go @@ -42,7 +42,7 @@ type mraidContext struct { func MraidPage(w http.ResponseWriter, r *http.Request) { body := `

MRAID Checker

-

Static checks for AppLovin, ironSource, and Unity HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.

+

Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.

@@ -112,8 +112,9 @@ document.getElementById('scan').addEventListener('click', async () => { const requirements = j.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'requirement').length, 0); const bestPractices = j.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'best-practice').length, 0); const skipped = Number(j.skipped || 0); - const skippedText = skipped ? ' Skipped ' + skipped + ' non-target HTML file(s).' : ''; - statusEl.innerHTML = '
Scanned ' + total + ' AppLovin/ironSource/Unity HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '
'; + const skippedReason = j.skippedReason || 'file(s).'; + const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : ''; + statusEl.innerHTML = '
Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '
'; resultsEl.innerHTML = ''; for (const rr of j.results) { const reqCount = rr.issues.filter(i => i.severity === 'requirement').length; @@ -164,6 +165,8 @@ func MraidScan(w http.ResponseWriter, r *http.Request) { var targets []string var baseDir string skipped := 0 + skippedReason := "" + noTargetsError := "No HTML files were found." if len(req.Files) > 0 { var existing []string for _, f := range req.Files { @@ -171,30 +174,30 @@ func MraidScan(w http.ResponseWriter, r *http.Request) { existing = append(existing, f) } } - for _, f := range existing { - if isSupportedMraidBuild(f) { - targets = append(targets, f) - } - } - skipped = len(existing) - len(targets) + targets = existing + skipped = len(req.Files) - len(existing) + skippedReason = "missing file(s)." + noTargetsError = "No selected HTML files were found." if len(targets) > 0 { baseDir = commonBaseDir(targets) } } else if req.Folder != "" && fileExists(req.Folder) { all := collectHTMLFiles(req.Folder) for _, f := range all { - if isSupportedMraidBuild(f) { + if isInSupportedMraidFolderPath(f) { targets = append(targets, f) } } skipped = len(all) - len(targets) + skippedReason = "HTML file(s) outside AppLovin/ironSource/Unity folders." + noTargetsError = "No AppLovin, ironSource, or Unity folder HTML files were found." baseDir = req.Folder } else { writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"}) return } if len(targets) == 0 { - writeJSON(w, map[string]any{"results": []mraidResult{}, "skipped": skipped, "error": "No AppLovin, ironSource, or Unity HTML builds were found."}) + writeJSON(w, map[string]any{"results": []mraidResult{}, "skipped": skipped, "skippedReason": skippedReason, "error": noTargetsError}) return } @@ -215,13 +218,12 @@ func MraidScan(w http.ResponseWriter, r *http.Request) { } results = append(results, mraidResult{File: display, OK: len(issues) == 0, Issues: issues}) } - writeJSON(w, map[string]any{"results": results, "skipped": skipped}) + writeJSON(w, map[string]any{"results": results, "skipped": skipped, "skippedReason": skippedReason}) } var ( mraidScriptRx = regexp.MustCompile(`(?is)]*>(.*?)`) mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`) - mraidBuildNameRx = regexp.MustCompile(`(?i)(^|[/._ -])(al|applovin|app-lovin|is|iron.?source|un|unity|unityads|unity-ads)([/._ -]|$)`) mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`) mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`) mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`) @@ -232,9 +234,30 @@ var ( const mraidReference = "MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/" -func isSupportedMraidBuild(filePath string) bool { - normalized := strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`) - return mraidBuildNameRx.MatchString(normalized) +func isInSupportedMraidFolderPath(filePath string) bool { + supported := map[string]bool{ + "al": true, + "applovin": true, + "app-lovin": true, + "is": true, + "ironsource": true, + "iron-source": true, + "iron_source": true, + "un": true, + "unity": true, + "unityads": true, + "unity-ads": true, + "unity_ads": true, + } + parts := strings.FieldsFunc(strings.ToLower(filepath.Dir(filePath)), func(r rune) bool { + return r == '/' || r == '\\' + }) + for _, part := range parts { + if supported[part] { + return true + } + } + return false } func checkMraid(htmlSrc string) []mraidIssue { diff --git a/standalone/playworks.go b/standalone/playworks.go index 8894457..8efc607 100644 --- a/standalone/playworks.go +++ b/standalone/playworks.go @@ -139,6 +139,11 @@ openOutBtn.addEventListener('click', () => { if (outputDir) fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: outputDir }) }); }); +function outputDisplayName(file) { + const parts = file.split(/[\\\\/]/).filter(Boolean); + return parts.slice(-2).join('/'); +} + document.getElementById('convert').addEventListener('click', async () => { const networks = [...document.querySelectorAll('.net-cb')].filter(c => c.checked).map(c => c.dataset.tag); statusEl.textContent = 'Converting...'; @@ -170,7 +175,7 @@ document.getElementById('convert').addEventListener('click', async () => { const detail = document.createElement('span'); if (r.ok) { detail.className = 'res-file'; - detail.textContent = r.file.split(/[\\\\/]/).pop(); + detail.textContent = outputDisplayName(r.file); } else { detail.className = 'res-err'; detail.textContent = r.error || 'Unknown error'; @@ -242,22 +247,51 @@ func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, netwo transformed := transformPlayworksHTML(htmlSrc, network) baseName := playworksSourceBaseName(opts.SourcePath) htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network) + outputDir := filepath.Join(opts.OutputDir, playworksNetworkOutputFolder(network)) + if err := os.MkdirAll(outputDir, 0755); err != nil { + return "", err + } if playworksNeedsZip(network) { - zipPath := filepath.Join(opts.OutputDir, fmt.Sprintf("%s_%s.zip", baseName, network)) + zipPath := filepath.Join(outputDir, fmt.Sprintf("%s_%s.zip", baseName, network)) if err := createSingleFileZip([]byte(transformed), "index.html", zipPath); err != nil { return "", err } return zipPath, nil } - outPath := filepath.Join(opts.OutputDir, htmlFileName) + outPath := filepath.Join(outputDir, htmlFileName) if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil { return "", err } return outPath, nil } +func playworksNetworkOutputFolder(network string) string { + switch network { + case "al": + return "Applovin" + case "is": + return "Ironsource" + case "un": + return "Unity" + case "fb": + return "Facebook" + case "gg": + return "GoogleAds" + case "mo": + return "Moloco" + case "vu": + return "Vungle" + case "mtg": + return "Mintegral" + case "tt": + return "TikTok" + default: + return network + } +} + func playworksNeedsZip(network string) bool { return network == "gg" || network == "vu" || network == "mtg" }