diff --git a/se5-plugins.json b/se5-plugins.json new file mode 100644 index 0000000..f75e10c --- /dev/null +++ b/se5-plugins.json @@ -0,0 +1,14 @@ +{ + "plugins": [ + { + "name": "Haxor", + "description": "Translates the text of the selected lines (or all lines) to haxor.", + "version": "1.0.0", + "author": "Subtitle Edit", + "url": "https://github.com/SubtitleEdit/plugins/tree/master/se5/Haxor", + "date": "2026-05-14", + "minSeVersion": "5.0.0", + "downloadUrl": "https://github.com/SubtitleEdit/plugins/releases/download/se5-v1.0/Haxor.zip" + } + ] +} diff --git a/se5/Haxor/Haxor.csproj b/se5/Haxor/Haxor.csproj new file mode 100644 index 0000000..ba47e41 --- /dev/null +++ b/se5/Haxor/Haxor.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + enable + Haxor + SubtitleEdit.Plugins.Haxor + true + + + diff --git a/se5/Haxor/HaxorTranslator.cs b/se5/Haxor/HaxorTranslator.cs new file mode 100644 index 0000000..f0e8a59 --- /dev/null +++ b/se5/Haxor/HaxorTranslator.cs @@ -0,0 +1,35 @@ +using System.Text; + +namespace SubtitleEdit.Plugins.Haxor; + +/// Lower-cases text and swaps a handful of letters for their "haxor" look-alikes. +public static class HaxorTranslator +{ + private static readonly Dictionary Map = new() + { + ['a'] = '4', + ['c'] = '©', + ['e'] = '3', + ['h'] = 'H', + ['i'] = '!', + ['k'] = 'K', + ['n'] = 'ñ', + ['o'] = '0', + ['s'] = '$', + ['y'] = '¥', + }; + + public static string Translate(string text) + { + var sb = new StringBuilder(text.ToLowerInvariant()); + for (var i = 0; i < sb.Length; i++) + { + if (Map.TryGetValue(sb[i], out var mapped)) + { + sb[i] = mapped; + } + } + + return sb.ToString(); + } +} diff --git a/se5/Haxor/PluginContract.cs b/se5/Haxor/PluginContract.cs new file mode 100644 index 0000000..3608724 --- /dev/null +++ b/se5/Haxor/PluginContract.cs @@ -0,0 +1,66 @@ +using System.Text.Json; + +namespace SubtitleEdit.Plugins.Haxor; + +// These types mirror the Subtitle Edit 5 plugin JSON contract. +// See https://github.com/SubtitleEdit/subtitleedit/blob/main/docs/plugin.md + +/// Written by Subtitle Edit; its path is passed as the first command-line argument. +public sealed class PluginRequest +{ + public int ApiVersion { get; set; } = 1; + public string RequestType { get; set; } = "run"; + + /// Where this plugin must write its . + public string ResponseFilePath { get; set; } = string.Empty; + + /// A scratch directory; deleted by Subtitle Edit after the run. + public string TempDirectory { get; set; } = string.Empty; + + public PluginSubtitle Subtitle { get; set; } = new(); + + /// Zero-based indices of the lines selected in the grid (empty if none). + public List SelectedIndices { get; set; } = new(); + + public string VideoFileName { get; set; } = string.Empty; + public double FrameRate { get; set; } + public string UiLanguage { get; set; } = string.Empty; + public string Theme { get; set; } = string.Empty; + public string SeVersion { get; set; } = string.Empty; + + /// This plugin's settings as last persisted by Subtitle Edit (null on first run). + public JsonElement? Settings { get; set; } +} + +public sealed class PluginSubtitle +{ + /// Friendly format name, e.g. "SubRip". + public string Format { get; set; } = string.Empty; + + public string FileName { get; set; } = string.Empty; + + /// Full subtitle text in . + public string Native { get; set; } = string.Empty; + + /// Full subtitle text as SubRip (.srt) - always provided in requests. + public string SubRip { get; set; } = string.Empty; +} + +/// Written by this plugin to . +public sealed class PluginResponse +{ + public int ApiVersion { get; set; } = 1; + + /// "ok": apply the subtitle; "cancelled": do nothing; "error": show the message. + public string Status { get; set; } = "cancelled"; + + public string? Message { get; set; } + + /// The modified subtitle. Only and are read. + public PluginSubtitle? Subtitle { get; set; } + + /// Settings to persist; handed back unchanged in the next request. + public JsonElement? Settings { get; set; } + + public string? UndoDescription { get; set; } +} diff --git a/se5/Haxor/Program.cs b/se5/Haxor/Program.cs new file mode 100644 index 0000000..79f79c1 --- /dev/null +++ b/se5/Haxor/Program.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using SubtitleEdit.Plugins.Haxor; + +// A Subtitle Edit 5 plugin is just an executable: +// 1. read the request file (its path is the first command-line argument), +// 2. transform the subtitle, +// 3. write the response file (path is given in the request), +// 4. exit with code 0. + +if (args.Length < 1) +{ + Console.Error.WriteLine("Usage: Haxor "); + return 1; +} + +var jsonOptions = new JsonSerializerOptions +{ + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, +}; + +PluginRequest? request; +try +{ + request = JsonSerializer.Deserialize(File.ReadAllText(args[0]), jsonOptions); +} +catch (Exception exception) +{ + Console.Error.WriteLine("Could not read request: " + exception.Message); + return 1; +} + +if (request is null || string.IsNullOrEmpty(request.ResponseFilePath)) +{ + Console.Error.WriteLine("Invalid request."); + return 1; +} + +// Work on the SubRip representation - it is always provided in the request. +var srt = request.Subtitle.SubRip; +var selected = new HashSet(request.SelectedIndices); + +var count = 0; +var blocks = Regex.Split(srt.Replace("\r\n", "\n").Trim('\n'), @"\n[ \t]*\n"); +for (var i = 0; i < blocks.Length; i++) +{ + // An empty SelectedIndices means "apply to every line". + if (selected.Count > 0 && !selected.Contains(i)) + { + continue; + } + + // A SubRip block is: number line, timecode line, then one or more text lines. + var lines = blocks[i].Split('\n'); + if (lines.Length < 3) + { + continue; + } + + for (var t = 2; t < lines.Length; t++) + { + lines[t] = HaxorTranslator.Translate(lines[t]); + } + + blocks[i] = string.Join('\n', lines); + count++; +} + +var response = new PluginResponse +{ + Status = "ok", + Message = count == 0 ? "No lines changed." : $"Translated {count} line(s) to haxor.", + UndoDescription = "Haxor 1.0.0", + Subtitle = new PluginSubtitle + { + Format = "SubRip", + Native = (string.Join("\n\n", blocks) + "\n").Replace("\n", "\r\n"), + }, +}; + +File.WriteAllText(request.ResponseFilePath, JsonSerializer.Serialize(response, jsonOptions)); +return 0; diff --git a/se5/Haxor/README.md b/se5/Haxor/README.md new file mode 100644 index 0000000..38e6ab9 --- /dev/null +++ b/se5/Haxor/README.md @@ -0,0 +1,49 @@ +# Haxor (Subtitle Edit 5 sample plugin) + +Translates subtitle text to "haxor" — lower-cases it and swaps a few letters for +look-alikes (`a→4`, `e→3`, `o→0`, `s→$`, `i→!`, `c→©`, `h→H`, `k→K`, `n→ñ`, `y→¥`). +If lines are selected in the grid only those are changed, otherwise every line is. + +This is a reference plugin for the Subtitle Edit 5 plugin system. It shows the whole +contract: read the request JSON, transform the subtitle, write the response JSON. +See [docs/plugin.md](https://github.com/SubtitleEdit/subtitleedit/blob/main/docs/plugin.md) +for the full specification. + +## Files + +| File | Purpose | +|------|---------| +| `plugin.json` | Manifest — lets Subtitle Edit list the plugin without launching it. | +| `PluginContract.cs` | The request/response DTOs (the JSON contract). | +| `Program.cs` | Entry point: read request → transform → write response → exit 0. | +| `HaxorTranslator.cs` | The actual text transformation. | + +## Build + +``` +dotnet publish Haxor.csproj -c Release -o publish +``` + +## Install for testing + +Copy the build output plus `plugin.json` into a folder under Subtitle Edit's +`Plugins` directory: + +``` +Plugins/ + Haxor/ + plugin.json + Haxor.dll + Haxor.runtimeconfig.json + ... (other files from publish/) +``` + +The manifest uses `"runtime": "dotnet"`, so Subtitle Edit launches it as +`dotnet Haxor.dll ` — the .NET 8 runtime must be installed. To ship a +self-contained build instead, publish with `-r --self-contained` and replace +the `runtime`/`entry` fields in `plugin.json` with an `executables` block. + +## Package for the plugin index + +Zip the plugin folder (so the zip contains a single top-level `Haxor/` folder), +attach it to a GitHub release, and point `downloadUrl` in `se5-plugins.json` at it. diff --git a/se5/Haxor/plugin.json b/se5/Haxor/plugin.json new file mode 100644 index 0000000..2844f75 --- /dev/null +++ b/se5/Haxor/plugin.json @@ -0,0 +1,12 @@ +{ + "apiVersion": 1, + "name": "Haxor", + "description": "Translates the text of the selected lines (or all lines) to haxor.", + "version": "1.0.0", + "author": "Subtitle Edit", + "url": "https://github.com/SubtitleEdit/plugins/tree/master/se5/Haxor", + "menu": "Translate", + "minSeVersion": "5.0.0", + "runtime": "dotnet", + "entry": "Haxor.dll" +}