feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
125
src/Paca.Browser/Content/MhtmlWebArchivePackager.cs
Normal file
125
src/Paca.Browser/Content/MhtmlWebArchivePackager.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Paca.Browser.Content;
|
||||
|
||||
public sealed class MhtmlWebArchivePackager
|
||||
{
|
||||
public string Package(WebArchivePackageRequest request)
|
||||
{
|
||||
var boundary = "----=_NextPart_" + Guid.NewGuid().ToString("N");
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("From: <Saved by PACA Browser>");
|
||||
sb.AppendLine($"Snapshot-Content-Location: {request.Url}");
|
||||
sb.AppendLine($"Subject: {request.Title}");
|
||||
sb.AppendLine($"Date: {DateTime.UtcNow:R}");
|
||||
sb.AppendLine("MIME-Version: 1.0");
|
||||
sb.AppendLine($"Content-Type: multipart/related; boundary=\"{boundary}\"; type=\"text/html\"");
|
||||
sb.AppendLine();
|
||||
|
||||
// HTML Part
|
||||
sb.AppendLine($"--{boundary}");
|
||||
sb.AppendLine("Content-Type: text/html; charset=\"utf-8\"");
|
||||
sb.AppendLine("Content-Transfer-Encoding: 8bit");
|
||||
sb.AppendLine($"Content-Location: {request.Url}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(request.HtmlContent);
|
||||
sb.AppendLine();
|
||||
|
||||
// Resources
|
||||
foreach (var res in request.Resources)
|
||||
{
|
||||
sb.AppendLine($"--{boundary}");
|
||||
sb.AppendLine($"Content-Type: {res.MimeType}");
|
||||
sb.AppendLine("Content-Transfer-Encoding: base64");
|
||||
sb.AppendLine($"Content-Location: {res.Url}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(Convert.ToBase64String(res.Data));
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine($"--{boundary}--");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public WebArchiveUnpackResult Unpack(string mhtmlContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mhtmlContent))
|
||||
{
|
||||
return new WebArchiveUnpackResult("Untitled", string.Empty, string.Empty, Array.Empty<WebArchiveResource>());
|
||||
}
|
||||
|
||||
var locMatch = Regex.Match(mhtmlContent, @"Snapshot-Content-Location:\s*([^\r\n]+)", RegexOptions.IgnoreCase);
|
||||
var locationUrl = locMatch.Success ? locMatch.Groups[1].Value.Trim() : string.Empty;
|
||||
|
||||
var subjMatch = Regex.Match(mhtmlContent, @"Subject:\s*([^\r\n]+)", RegexOptions.IgnoreCase);
|
||||
var title = subjMatch.Success ? subjMatch.Groups[1].Value.Trim() : "Untitled";
|
||||
|
||||
var boundMatch = Regex.Match(mhtmlContent, @"boundary=""?([^"";\r\n]+)""?", RegexOptions.IgnoreCase);
|
||||
if (!boundMatch.Success)
|
||||
{
|
||||
return new WebArchiveUnpackResult(title, locationUrl, mhtmlContent, Array.Empty<WebArchiveResource>());
|
||||
}
|
||||
|
||||
var boundary = boundMatch.Groups[1].Value;
|
||||
var parts = mhtmlContent.Split(new[] { "--" + boundary }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
string htmlContent = string.Empty;
|
||||
var resources = new List<WebArchiveResource>();
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.StartsWith("--") || string.IsNullOrWhiteSpace(part)) continue;
|
||||
|
||||
var doubleLine = part.IndexOf("\r\n\r\n", StringComparison.Ordinal);
|
||||
if (doubleLine < 0) doubleLine = part.IndexOf("\n\n", StringComparison.Ordinal);
|
||||
if (doubleLine < 0) continue;
|
||||
|
||||
var headers = part.Substring(0, doubleLine);
|
||||
var body = part.Substring(doubleLine).Trim();
|
||||
|
||||
var typeMatch = Regex.Match(headers, @"Content-Type:\s*([^;\r\n]+)", RegexOptions.IgnoreCase);
|
||||
var mimeType = typeMatch.Success ? typeMatch.Groups[1].Value.Trim() : "text/html";
|
||||
|
||||
if (mimeType.Contains("multipart/related", StringComparison.OrdinalIgnoreCase) ||
|
||||
headers.Contains("boundary=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var urlMatch = Regex.Match(headers, @"Content-Location:\s*([^\r\n]+)", RegexOptions.IgnoreCase);
|
||||
var resUrl = urlMatch.Success ? urlMatch.Groups[1].Value.Trim() : string.Empty;
|
||||
|
||||
if (mimeType.Contains("text/html", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
htmlContent = body;
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] data;
|
||||
if (headers.Contains("base64", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
data = Convert.FromBase64String(body);
|
||||
}
|
||||
catch
|
||||
{
|
||||
data = Encoding.UTF8.GetBytes(body);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data = Encoding.UTF8.GetBytes(body);
|
||||
}
|
||||
|
||||
resources.Add(new WebArchiveResource(resUrl, mimeType, data));
|
||||
}
|
||||
}
|
||||
|
||||
return new WebArchiveUnpackResult(title, locationUrl, htmlContent, resources.AsReadOnly());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue