Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ jobs:
run: dotnet restore

- name: Build
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0-windows
run: dotnet publish ./src/Compiler/Compiler.csproj -c Release -r win-x64 -f net10.0

- uses: actions/upload-artifact@v4
with:
Expand Down
33 changes: 33 additions & 0 deletions src/Compiler/CompilerSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 James Draycott <me@racci.dev>. All Rights Reserved.
// Licensed under the AGPL-3.0-or-later License, See LICENSE in the project root
// for license information.

using System.IO.Compression;

namespace Compiler;

internal enum EmbeddedLocalTextCompression {
None,
GZip
}

internal static class CompilerSettings {
internal static EmbeddedLocalTextCompression EmbeddedLocalTextCompression { get; set; } = EmbeddedLocalTextCompression.GZip;

internal static CompressionLevel EmbeddedLocalTextCompressionLevel { get; set; } = CompressionLevel.Optimal;

internal static void ConfigureEmbeddedLocalTextCompression(string mode) {
switch (mode.ToLowerInvariant()) {
case "none":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.None;
EmbeddedLocalTextCompressionLevel = CompressionLevel.NoCompression;
break;
case "gzip":
EmbeddedLocalTextCompression = EmbeddedLocalTextCompression.GZip;
EmbeddedLocalTextCompressionLevel = CompressionLevel.Optimal;
break;
default:
throw new ArgumentException($"Unsupported embedded compression mode '{mode}'. Use none or gzip.");
}
}
}
12 changes: 11 additions & 1 deletion src/Compiler/Module/Compiled/Compiled.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,10 +16,14 @@ namespace Compiler.Module.Compiled;

public enum ContentType {
UTF8String,
Base64Utf8,
Zip
}

public enum ContentCompression {
None,
GZip
}

[method: Pure]
public abstract class Compiled(ModuleSpec moduleSpec, RequirementGroup requirements) {
public Compiled(
Expand DownExpand Up@@ -87,6 +91,11 @@ Fin<string> ComputeHash() {
/// </summary>
public abstract ContentType Type { get; }

/// <summary>
/// Determines how the content bytes of this module should be decompressed.
/// </summary>
public abstract ContentCompression Compression { get; }

public virtual Fin<string> GetIdentityHash() => this.ComputedHash();

public Fin<string> GetNameHash() => this.GetIdentityHash().Map(hash => $"{this.ModuleSpec.Name}-{hash[..6]}");
Expand All@@ -110,6 +119,7 @@ from hash in this.GetIdentityHash()
Version = '{{this.Version}}';
Hash = '{{hash[..6]}}';
Type = '{{this.Type}}';
Compression = '{{this.Compression}}';
Content = {{content}}
Comment thread
DaRacci marked this conversation as resolved.
}
""";
Expand Down
40 changes: 33 additions & 7 deletions src/Compiler/Module/Compiled/Local.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO.Compression;
using System.Text;
using Compiler.Requirements;
using Compiler.Text;
Expand All@@ -13,31 +14,39 @@
namespace Compiler.Module.Compiled;

public class CompiledLocalModule : Compiled {
public override ContentType Type { get; } = ContentType.Base64Utf8;
public override ContentType Type { get; } = ContentType.UTF8String;

public override ContentCompression Compression { get; }

// Local modules are always version 0.0.1, as they are not versioned.
public override Version Version { get; } = new Version(0, 0, 1);

public virtual CompiledDocument Document { get; }

[Pure]
private readonly EmbeddedLocalTextCompression CompressionMode;

private readonly CompressionLevel CompressionLevel;

public CompiledLocalModule(
PathedModuleSpec moduleSpec,
CompiledDocument document,
RequirementGroup requirements
) : base(moduleSpec, requirements) {
this.Document = document;
this.SetContentBytes(new(() => this.GetRawContentText().Map(text => Encoding.UTF8.GetBytes(text))));
this.CompressionMode = CompilerSettings.EmbeddedLocalTextCompression;
this.CompressionLevel = CompilerSettings.EmbeddedLocalTextCompressionLevel;
this.Compression = this.CompressionMode == EmbeddedLocalTextCompression.None ? ContentCompression.None : ContentCompression.GZip;
this.SetContentBytes(new(this.GetRawContentBytes));
}

[Pure]
protected virtual Fin<string> GetRawContentText() {
protected virtual Fin<byte[]> GetRawContentBytes() {
var content = new StringBuilder();

foreach (var requirement in this.Requirements.GetRequirements()) {
var hashResult = requirement switch {
ModuleSpec req => this.FindSibling(req) is { } sibling
? sibling.GetIdentityHash().Map(hash => hash[..6])
? sibling.GetNameHash().Map(hash => hash[(sibling.ModuleSpec.Name.Length + 1)..])
: Fin.Fail<string>(Error.New($"Missing compiled sibling for module requirement {requirement} in {this.ModuleSpec.Name}.")),
_ => Pure(requirement.HashString[..6])
};
Expand All@@ -53,11 +62,28 @@ protected virtual Fin<string> GetRawContentText() {
content.AppendLine()
.Append(this.Document.GetContent());

return content.ToString();
return Encoding.UTF8.GetBytes(content.ToString());
}

public override Fin<string> StringifyContent() =>
this.GetRawContentText().Map(text => $"'{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}'");
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? $"'{Encoding.UTF8.GetString(bytes).Replace("'", "''")}'"
: $"'{Convert.ToBase64String(Compress(bytes, this.CompressionLevel))}'");

/// <summary>Returns the raw payload bytes as they appear in the embedded output (compressed or raw).</summary>
internal Fin<byte[]> GetEmbeddedPayloadBytes() =>
this.GetRawContentBytes().Map(bytes => this.CompressionMode == EmbeddedLocalTextCompression.None
? bytes
: Compress(bytes, this.CompressionLevel));

protected static byte[] Compress(byte[] bytes, CompressionLevel compressionLevel) {
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, compressionLevel, true)) {
gzip.Write(bytes, 0, bytes.Length);
}

return output.ToArray();
}

public Fin<Unit> ValidateRequirementsResolved() {
foreach (var requirement in this.Requirements.GetRequirements<ModuleSpec>()) {
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Module/Compiled/Remote.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,10 @@

using System.Collections;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CommandLine;
Expand DownExpand Up@@ -39,11 +39,12 @@ private sealed record ExtraModuleInfo(

private Lock UpdatingArchiveLock { get; } = new();
private Option<byte[]> UpdatedContentBytes;

private readonly Fin<string> IdentityHash;

public override ContentType Type => ContentType.Zip;

public override ContentCompression Compression => ContentCompression.None;

public override Version Version { get; }

public CompiledRemoteModule(
Expand Down
33 changes: 25 additions & 8 deletions src/Compiler/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
using System.Text;
using CommandLine;
using Compiler.Analyser;
using Compiler.Module.Compiled;
using Compiler.Module.Resolvable;
using Compiler.Requirements;
using Extended.Collections.Generic;
Expand DownExpand Up@@ -65,6 +66,9 @@ public class Options {

[Option('f', "force", Required = false, HelpText = "Force overwrite of output file.")]
public bool Force { get; set; }

[Option("embedded-compression", Required = false, Default = "gzip", HelpText = "Embedded local text compression mode: none|gzip.")]
public string EmbeddedCompression { get; set; } = "gzip";
}

private static async Task<int> Main(string[] args) {
Expand All@@ -78,7 +82,12 @@ private static async Task<int> Main(string[] args) {
async opts => {
CleanInput(opts);
IsDebugging = SetupLogger(opts) <= LogLevel.Debug;

try {
CompilerSettings.ConfigureEmbeddedLocalTextCompression(opts.EmbeddedCompression);
} catch (ArgumentException ex) {
Errors.Add(ex);
return;
}
if (GetFilesToCompile(opts.Input!).IsErr(out var error, out var filesToCompile)) {
Errors.Add(error);
return;
Expand All@@ -92,8 +101,8 @@ private static async Task<int> Main(string[] args) {
var scriptCreationTasks = filesToCompile.Select(async scriptPath => {
var pathedModuleSpec = new PathedModuleSpec(sourceRoot, Path.GetFullPath(scriptPath));
var maybeScript = await Resolvable.TryCreateScript(pathedModuleSpec, superParent);
if (maybeScript.IsErr(out var error, out var resolvableScript)) {
Errors.Add(error.Enrich(pathedModuleSpec));
if (maybeScript.IsErr(out var err, out var resolvableScript)) {
Errors.Add(err.Enrich(pathedModuleSpec));
return;
}

Expand All@@ -109,6 +118,8 @@ await Output(
scriptPath,
output,
opts.Force);

LogCompressionSummary(compiled);
});
}).ToArray();

Expand All@@ -125,10 +136,7 @@ await Output(
Option<string> sourceDirectory = None;
Option<string> outputDirectory = None;
if (result.Value.AsOption().IsSome(out var opts)) {
sourceDirectory = opts.Input.AsOption().Map(input => {
return File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input;
})!;

sourceDirectory = opts.Input.AsOption().Map(_ => File.Exists(opts.Input) ? Path.GetDirectoryName(opts.Input)! : opts.Input)!;
outputDirectory = opts.Output.AsOption().Map(Path.GetFullPath);
}
await OutputErrors(Errors, sourceDirectory, outputDirectory);
Expand All@@ -142,6 +150,16 @@ await Output(
return Errors.IsEmpty ? 0 : Errors.All(e => !e.IsExceptional) ? 0 : 1;
}

private static void LogCompressionSummary(CompiledScript compiled) {
var localModules = compiled.Graph.Vertices.OfType<CompiledLocalModule>().ToArray();
var rawBytes = localModules.Sum(module => module.GetContentBytes().Match(bytes => bytes.Length, _ => 0));
var payloadBytes = localModules.Sum(module => module.GetEmbeddedPayloadBytes().Match(bytes => bytes.Length, _ => 0));
var serializedBytes = compiled.GetPowerShellObject().Match(Encoding.UTF8.GetByteCount, _ => 0);
var savings = rawBytes == 0 ? 0d : 1d - ((double)payloadBytes / rawBytes);

Logger.Info($"Compression summary: mode={CompilerSettings.EmbeddedLocalTextCompression}, local_modules={localModules.Length}, raw_bytes={rawBytes}, payload_bytes={payloadBytes}, serialized_bytes={serializedBytes}, savings={savings:P1}");
}

public static void CleanInput(Options opts) {
ArgumentException.ThrowIfNullOrWhiteSpace(opts.Input, nameof(opts.Input));

Expand DownExpand Up@@ -336,7 +354,6 @@ bool forceOverwrite
var outputPath = GetOutputLocation(sourceDirectory, outputDirectory, fileName);
Logger.Debug($"Preparing output for {fileName} -> {outputPath}");
if (File.Exists(outputPath)) {

var hashEngine = System.Security.Cryptography.SHA256.Create();
var existingFileStream = File.OpenRead(outputPath);
var hash = hashEngine.ComputeHash(existingFileStream);
Expand Down
Loading
Loading