2 Commits

Author SHA1 Message Date
92de00b52e 0.1.6
- cleanup per network folder for playworks converter
- loosen mraid checker file and folder scanning
2026-05-27 19:08:15 +08:00
78578cec16 0.1.6
- playworks converter (beta)
- mraid checker (beta)
- removed daily updates generator
2026-05-27 18:41:26 +08:00
7 changed files with 137 additions and 37 deletions

View File

@@ -2,7 +2,7 @@
"name": "hpl-toolbox", "name": "hpl-toolbox",
"displayName": "HPL Toolbox", "displayName": "HPL Toolbox",
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, MRAID Checker. Local HTML Host.", "description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, MRAID Checker. Local HTML Host.",
"version": "0.1.5", "version": "0.1.6",
"publisher": "hesukastro", "publisher": "hesukastro",
"license": "UNLICENSED", "license": "UNLICENSED",
"repository": { "repository": {
@@ -12,7 +12,9 @@
"engines": { "engines": {
"vscode": "^1.85.0" "vscode": "^1.85.0"
}, },
"categories": ["Other"], "categories": [
"Other"
],
"icon": "media/hpl.png", "icon": "media/hpl.png",
"main": "./out/extension.js", "main": "./out/extension.js",
"activationEvents": [], "activationEvents": [],

View File

@@ -80,15 +80,21 @@ export function openMraidChecker(_context: vscode.ExtensionContext) {
let targets: string[] = []; let targets: string[] = [];
let baseDir = ''; let baseDir = '';
let skipped = 0; let skipped = 0;
let skippedReason = '';
let noTargetsError = 'No HTML files were found.';
if (files && files.length) { if (files && files.length) {
const existing = files.filter(f => fs.existsSync(f)); const existing = files.filter(f => fs.existsSync(f));
targets = existing.filter(isSupportedMraidBuild); targets = existing;
skipped = existing.length - targets.length; skipped = files.length - existing.length;
skippedReason = 'missing file(s).';
noTargetsError = 'No selected HTML files were found.';
baseDir = targets.length ? commonBaseDir(targets) : ''; baseDir = targets.length ? commonBaseDir(targets) : '';
} else if (folder && fs.existsSync(folder)) { } else if (folder && fs.existsSync(folder)) {
const allHtmlFiles = await collectHtmlFiles(folder); const allHtmlFiles = await collectHtmlFiles(folder);
targets = allHtmlFiles.filter(isSupportedMraidBuild); targets = allHtmlFiles.filter(isInSupportedMraidFolderPath);
skipped = allHtmlFiles.length - targets.length; 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; baseDir = folder;
} else { } else {
panel.webview.postMessage({ type: 'results', results: [], error: 'Pick a folder or files first' }); 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', type: 'results',
results: [], results: [],
skipped, skipped,
error: 'No AppLovin, ironSource, or Unity HTML builds were found.', skippedReason,
error: noTargetsError,
}); });
return; return;
} }
@@ -116,7 +123,7 @@ export function openMraidChecker(_context: vscode.ExtensionContext) {
// Skip unreadable files, matching the scanner pattern used elsewhere. // 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<string, number> {
return calls; return calls;
} }
function isSupportedMraidBuild(filePath: string): boolean { function isInSupportedMraidFolderPath(filePath: string): boolean {
const normalized = filePath.toLowerCase().replace(/\\/g, '/'); const folderParts = path.dirname(filePath)
return /(^|[\/._ -])(al|applovin|app-lovin|is|iron.?source|un|unity|unityads|unity-ads)([\/._ -]|$)/i.test(normalized); .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 { function hasMraidReference(ctx: ScanContext): boolean {
@@ -387,7 +409,7 @@ function getHtml(): string {
</head> </head>
<body> <body>
<h2>MRAID Checker</h2> <h2>MRAID Checker</h2>
<div class="docs">Static checks for AppLovin, ironSource, and Unity HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</div> <div class="docs">Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</div>
<div class="row"> <div class="row">
<input id="folder" type="text" placeholder="Folder to scan recursively..." /> <input id="folder" type="text" placeholder="Folder to scan recursively..." />
<button id="pick">Browse Folder...</button> <button id="pick">Browse Folder...</button>
@@ -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 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 bestPractices = msg.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'best-practice').length, 0);
const skipped = Number(msg.skipped || 0); const skipped = Number(msg.skipped || 0);
const skippedText = skipped ? ' Skipped ' + skipped + ' non-target HTML file(s).' : ''; const skippedReason = msg.skippedReason || 'file(s).';
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' AppLovin/ironSource/Unity HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>'; const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : '';
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>';
resultsEl.innerHTML = ''; resultsEl.innerHTML = '';
for (const r of msg.results) { for (const r of msg.results) {
const requirementsForFile = r.issues.filter(i => i.severity === 'requirement').length; const requirementsForFile = r.issues.filter(i => i.severity === 'requirement').length;

View File

@@ -94,6 +94,17 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) {
} }
const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']); const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']);
const NETWORK_OUTPUT_FOLDERS: Record<string, string> = {
al: 'Applovin',
is: 'Ironsource',
un: 'Unity',
fb: 'Facebook',
gg: 'GoogleAds',
mo: 'Moloco',
vu: 'Vungle',
mtg: 'Mintegral',
tt: 'TikTok',
};
function sourceBaseName(sourcePath: string): string { function sourceBaseName(sourcePath: string): string {
return path.basename(sourcePath, path.extname(sourcePath)) 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 needsZip = ZIPPED_NETWORKS.has(network);
const baseName = sourceBaseName(opts.sourcePath); const baseName = sourceBaseName(opts.sourcePath);
const htmlFileName = `${baseName}_${network}.html`; const htmlFileName = `${baseName}_${network}.html`;
const outputDir = path.join(opts.outputDir, NETWORK_OUTPUT_FOLDERS[network] ?? network);
fs.mkdirSync(outputDir, { recursive: true });
if (needsZip) { if (needsZip) {
const tmpPath = path.join(opts.outputDir, `_tmp_${Date.now()}_${network}.html`); const tmpPath = path.join(outputDir, `_tmp_${Date.now()}_${network}.html`);
const zipPath = path.join(opts.outputDir, `${baseName}_${network}.zip`); const zipPath = path.join(outputDir, `${baseName}_${network}.zip`);
fs.writeFileSync(tmpPath, transformed, 'utf8'); fs.writeFileSync(tmpPath, transformed, 'utf8');
try { try {
await createZip(tmpPath, 'index.html', zipPath); await createZip(tmpPath, 'index.html', zipPath);
@@ -118,7 +131,7 @@ async function convertNetwork(html: string, opts: ConvertOptions, network: strin
return zipPath; return zipPath;
} }
const outPath = path.join(opts.outputDir, htmlFileName); const outPath = path.join(outputDir, htmlFileName);
fs.writeFileSync(outPath, transformed, 'utf8'); fs.writeFileSync(outPath, transformed, 'utf8');
return outPath; return outPath;
} }
@@ -431,6 +444,11 @@ function getHtml(): string {
if (outputDir) vscode.postMessage({ type: 'openOutput', dir: outputDir }); 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', () => { document.getElementById('convert').addEventListener('click', () => {
const networks = [...document.querySelectorAll('.net-cb')] const networks = [...document.querySelectorAll('.net-cb')]
.filter(c => c.checked).map(c => c.dataset.tag); .filter(c => c.checked).map(c => c.dataset.tag);
@@ -484,7 +502,7 @@ function getHtml(): string {
const detail = document.createElement('span'); const detail = document.createElement('span');
if (r.ok) { if (r.ok) {
detail.className = 'res-file'; detail.className = 'res-file';
detail.textContent = r.file.split(/[\\\\/]/).pop(); detail.textContent = outputDisplayName(r.file);
} else { } else {
detail.className = 'res-err'; detail.className = 'res-err';
detail.textContent = r.error || 'Unknown error'; detail.textContent = r.error || 'Unknown error';

View File

@@ -42,7 +42,7 @@ type mraidContext struct {
func MraidPage(w http.ResponseWriter, r *http.Request) { func MraidPage(w http.ResponseWriter, r *http.Request) {
body := ` body := `
<h2>MRAID Checker</h2> <h2>MRAID Checker</h2>
<p class="hint">Static checks for AppLovin, ironSource, and Unity HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p> <p class="hint">Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;"> <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
<input id="folder" type="text" placeholder="Folder to scan recursively..." style="flex:1;" /> <input id="folder" type="text" placeholder="Folder to scan recursively..." style="flex:1;" />
<button id="pickFolder">Browse Folder...</button> <button id="pickFolder">Browse Folder...</button>
@@ -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 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 bestPractices = j.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'best-practice').length, 0);
const skipped = Number(j.skipped || 0); const skipped = Number(j.skipped || 0);
const skippedText = skipped ? ' Skipped ' + skipped + ' non-target HTML file(s).' : ''; const skippedReason = j.skippedReason || 'file(s).';
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' AppLovin/ironSource/Unity HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>'; const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : '';
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>';
resultsEl.innerHTML = ''; resultsEl.innerHTML = '';
for (const rr of j.results) { for (const rr of j.results) {
const reqCount = rr.issues.filter(i => i.severity === 'requirement').length; 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 targets []string
var baseDir string var baseDir string
skipped := 0 skipped := 0
skippedReason := ""
noTargetsError := "No HTML files were found."
if len(req.Files) > 0 { if len(req.Files) > 0 {
var existing []string var existing []string
for _, f := range req.Files { for _, f := range req.Files {
@@ -171,30 +174,30 @@ func MraidScan(w http.ResponseWriter, r *http.Request) {
existing = append(existing, f) existing = append(existing, f)
} }
} }
for _, f := range existing { targets = existing
if isSupportedMraidBuild(f) { skipped = len(req.Files) - len(existing)
targets = append(targets, f) skippedReason = "missing file(s)."
} noTargetsError = "No selected HTML files were found."
}
skipped = len(existing) - len(targets)
if len(targets) > 0 { if len(targets) > 0 {
baseDir = commonBaseDir(targets) baseDir = commonBaseDir(targets)
} }
} else if req.Folder != "" && fileExists(req.Folder) { } else if req.Folder != "" && fileExists(req.Folder) {
all := collectHTMLFiles(req.Folder) all := collectHTMLFiles(req.Folder)
for _, f := range all { for _, f := range all {
if isSupportedMraidBuild(f) { if isInSupportedMraidFolderPath(f) {
targets = append(targets, f) targets = append(targets, f)
} }
} }
skipped = len(all) - len(targets) 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 baseDir = req.Folder
} else { } else {
writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"}) writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"})
return return
} }
if len(targets) == 0 { 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 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}) 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 ( var (
mraidScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>(.*?)</script>`) mraidScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>(.*?)</script>`)
mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`) 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`) mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`)
mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`) mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`)
mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`) 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/" const mraidReference = "MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/"
func isSupportedMraidBuild(filePath string) bool { func isInSupportedMraidFolderPath(filePath string) bool {
normalized := strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`) supported := map[string]bool{
return mraidBuildNameRx.MatchString(normalized) "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 { func checkMraid(htmlSrc string) []mraidIssue {

View File

@@ -139,6 +139,11 @@ openOutBtn.addEventListener('click', () => {
if (outputDir) fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: outputDir }) }); 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 () => { document.getElementById('convert').addEventListener('click', async () => {
const networks = [...document.querySelectorAll('.net-cb')].filter(c => c.checked).map(c => c.dataset.tag); const networks = [...document.querySelectorAll('.net-cb')].filter(c => c.checked).map(c => c.dataset.tag);
statusEl.textContent = 'Converting...'; statusEl.textContent = 'Converting...';
@@ -170,7 +175,7 @@ document.getElementById('convert').addEventListener('click', async () => {
const detail = document.createElement('span'); const detail = document.createElement('span');
if (r.ok) { if (r.ok) {
detail.className = 'res-file'; detail.className = 'res-file';
detail.textContent = r.file.split(/[\\\\/]/).pop(); detail.textContent = outputDisplayName(r.file);
} else { } else {
detail.className = 'res-err'; detail.className = 'res-err';
detail.textContent = r.error || 'Unknown error'; detail.textContent = r.error || 'Unknown error';
@@ -242,22 +247,51 @@ func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, netwo
transformed := transformPlayworksHTML(htmlSrc, network) transformed := transformPlayworksHTML(htmlSrc, network)
baseName := playworksSourceBaseName(opts.SourcePath) baseName := playworksSourceBaseName(opts.SourcePath)
htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network) 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) { 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 { if err := createSingleFileZip([]byte(transformed), "index.html", zipPath); err != nil {
return "", err return "", err
} }
return zipPath, nil return zipPath, nil
} }
outPath := filepath.Join(opts.OutputDir, htmlFileName) outPath := filepath.Join(outputDir, htmlFileName)
if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil { if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil {
return "", err return "", err
} }
return outPath, nil 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 { func playworksNeedsZip(network string) bool {
return network == "gg" || network == "vu" || network == "mtg" return network == "gg" || network == "vu" || network == "mtg"
} }