Skip to content

Commit 5ab8841

Browse files
authored
Merge pull request #268 from SubtitleEdit/add-se5-plugins
Add Subtitle Edit 5 plugin index and Haxor sample plugin
2 parents ba2289c + 01f28e5 commit 5ab8841

7 files changed

Lines changed: 275 additions & 0 deletions

File tree

se5-plugins.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"plugins": [
3+
{
4+
"name": "Haxor",
5+
"description": "Translates the text of the selected lines (or all lines) to haxor.",
6+
"version": "1.0.0",
7+
"author": "Subtitle Edit",
8+
"url": "https://github.com/SubtitleEdit/plugins/tree/master/se5/Haxor",
9+
"date": "2026-05-14",
10+
"minSeVersion": "5.0.0",
11+
"downloadUrl": "https://github.com/SubtitleEdit/plugins/releases/download/se5-v1.0/Haxor.zip"
12+
}
13+
]
14+
}

se5/Haxor/Haxor.csproj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<AssemblyName>Haxor</AssemblyName>
9+
<RootNamespace>SubtitleEdit.Plugins.Haxor</RootNamespace>
10+
<InvariantGlobalization>true</InvariantGlobalization>
11+
</PropertyGroup>
12+
13+
</Project>

se5/Haxor/HaxorTranslator.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Text;
2+
3+
namespace SubtitleEdit.Plugins.Haxor;
4+
5+
/// <summary>Lower-cases text and swaps a handful of letters for their "haxor" look-alikes.</summary>
6+
public static class HaxorTranslator
7+
{
8+
private static readonly Dictionary<char, char> Map = new()
9+
{
10+
['a'] = '4',
11+
['c'] = '©',
12+
['e'] = '3',
13+
['h'] = 'H',
14+
['i'] = '!',
15+
['k'] = 'K',
16+
['n'] = 'ñ',
17+
['o'] = '0',
18+
['s'] = '$',
19+
['y'] = '¥',
20+
};
21+
22+
public static string Translate(string text)
23+
{
24+
var sb = new StringBuilder(text.ToLowerInvariant());
25+
for (var i = 0; i < sb.Length; i++)
26+
{
27+
if (Map.TryGetValue(sb[i], out var mapped))
28+
{
29+
sb[i] = mapped;
30+
}
31+
}
32+
33+
return sb.ToString();
34+
}
35+
}

se5/Haxor/PluginContract.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Text.Json;
2+
3+
namespace SubtitleEdit.Plugins.Haxor;
4+
5+
// These types mirror the Subtitle Edit 5 plugin JSON contract.
6+
// See https://github.com/SubtitleEdit/subtitleedit/blob/main/docs/plugin.md
7+
8+
/// <summary>Written by Subtitle Edit; its path is passed as the first command-line argument.</summary>
9+
public sealed class PluginRequest
10+
{
11+
public int ApiVersion { get; set; } = 1;
12+
public string RequestType { get; set; } = "run";
13+
14+
/// <summary>Where this plugin must write its <see cref="PluginResponse"/>.</summary>
15+
public string ResponseFilePath { get; set; } = string.Empty;
16+
17+
/// <summary>A scratch directory; deleted by Subtitle Edit after the run.</summary>
18+
public string TempDirectory { get; set; } = string.Empty;
19+
20+
public PluginSubtitle Subtitle { get; set; } = new();
21+
22+
/// <summary>Zero-based indices of the lines selected in the grid (empty if none).</summary>
23+
public List<int> SelectedIndices { get; set; } = new();
24+
25+
public string VideoFileName { get; set; } = string.Empty;
26+
public double FrameRate { get; set; }
27+
public string UiLanguage { get; set; } = string.Empty;
28+
public string Theme { get; set; } = string.Empty;
29+
public string SeVersion { get; set; } = string.Empty;
30+
31+
/// <summary>This plugin's settings as last persisted by Subtitle Edit (null on first run).</summary>
32+
public JsonElement? Settings { get; set; }
33+
}
34+
35+
public sealed class PluginSubtitle
36+
{
37+
/// <summary>Friendly format name, e.g. "SubRip".</summary>
38+
public string Format { get; set; } = string.Empty;
39+
40+
public string FileName { get; set; } = string.Empty;
41+
42+
/// <summary>Full subtitle text in <see cref="Format"/>.</summary>
43+
public string Native { get; set; } = string.Empty;
44+
45+
/// <summary>Full subtitle text as SubRip (.srt) - always provided in requests.</summary>
46+
public string SubRip { get; set; } = string.Empty;
47+
}
48+
49+
/// <summary>Written by this plugin to <see cref="PluginRequest.ResponseFilePath"/>.</summary>
50+
public sealed class PluginResponse
51+
{
52+
public int ApiVersion { get; set; } = 1;
53+
54+
/// <summary>"ok": apply the subtitle; "cancelled": do nothing; "error": show the message.</summary>
55+
public string Status { get; set; } = "cancelled";
56+
57+
public string? Message { get; set; }
58+
59+
/// <summary>The modified subtitle. Only <see cref="PluginSubtitle.Format"/> and <see cref="PluginSubtitle.Native"/> are read.</summary>
60+
public PluginSubtitle? Subtitle { get; set; }
61+
62+
/// <summary>Settings to persist; handed back unchanged in the next request.</summary>
63+
public JsonElement? Settings { get; set; }
64+
65+
public string? UndoDescription { get; set; }
66+
}

se5/Haxor/Program.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using System.Text.RegularExpressions;
4+
using SubtitleEdit.Plugins.Haxor;
5+
6+
// A Subtitle Edit 5 plugin is just an executable:
7+
// 1. read the request file (its path is the first command-line argument),
8+
// 2. transform the subtitle,
9+
// 3. write the response file (path is given in the request),
10+
// 4. exit with code 0.
11+
12+
if (args.Length < 1)
13+
{
14+
Console.Error.WriteLine("Usage: Haxor <requestFilePath>");
15+
return 1;
16+
}
17+
18+
var jsonOptions = new JsonSerializerOptions
19+
{
20+
PropertyNameCaseInsensitive = true,
21+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
22+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
23+
WriteIndented = true,
24+
};
25+
26+
PluginRequest? request;
27+
try
28+
{
29+
request = JsonSerializer.Deserialize<PluginRequest>(File.ReadAllText(args[0]), jsonOptions);
30+
}
31+
catch (Exception exception)
32+
{
33+
Console.Error.WriteLine("Could not read request: " + exception.Message);
34+
return 1;
35+
}
36+
37+
if (request is null || string.IsNullOrEmpty(request.ResponseFilePath))
38+
{
39+
Console.Error.WriteLine("Invalid request.");
40+
return 1;
41+
}
42+
43+
// Work on the SubRip representation - it is always provided in the request.
44+
var srt = request.Subtitle.SubRip;
45+
var selected = new HashSet<int>(request.SelectedIndices);
46+
47+
var count = 0;
48+
var blocks = Regex.Split(srt.Replace("\r\n", "\n").Trim('\n'), @"\n[ \t]*\n");
49+
for (var i = 0; i < blocks.Length; i++)
50+
{
51+
// An empty SelectedIndices means "apply to every line".
52+
if (selected.Count > 0 && !selected.Contains(i))
53+
{
54+
continue;
55+
}
56+
57+
// A SubRip block is: number line, timecode line, then one or more text lines.
58+
var lines = blocks[i].Split('\n');
59+
if (lines.Length < 3)
60+
{
61+
continue;
62+
}
63+
64+
for (var t = 2; t < lines.Length; t++)
65+
{
66+
lines[t] = HaxorTranslator.Translate(lines[t]);
67+
}
68+
69+
blocks[i] = string.Join('\n', lines);
70+
count++;
71+
}
72+
73+
var response = new PluginResponse
74+
{
75+
Status = "ok",
76+
Message = count == 0 ? "No lines changed." : $"Translated {count} line(s) to haxor.",
77+
UndoDescription = "Haxor 1.0.0",
78+
Subtitle = new PluginSubtitle
79+
{
80+
Format = "SubRip",
81+
Native = (string.Join("\n\n", blocks) + "\n").Replace("\n", "\r\n"),
82+
},
83+
};
84+
85+
File.WriteAllText(request.ResponseFilePath, JsonSerializer.Serialize(response, jsonOptions));
86+
return 0;

se5/Haxor/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Haxor (Subtitle Edit 5 sample plugin)
2+
3+
Translates subtitle text to "haxor" — lower-cases it and swaps a few letters for
4+
look-alikes (`a→4`, `e→3`, `o→0`, `s→$`, `i→!`, `c→©`, `h→H`, `k→K`, `n→ñ`, `y→¥`).
5+
If lines are selected in the grid only those are changed, otherwise every line is.
6+
7+
This is a reference plugin for the Subtitle Edit 5 plugin system. It shows the whole
8+
contract: read the request JSON, transform the subtitle, write the response JSON.
9+
See [docs/plugin.md](https://github.com/SubtitleEdit/subtitleedit/blob/main/docs/plugin.md)
10+
for the full specification.
11+
12+
## Files
13+
14+
| File | Purpose |
15+
|------|---------|
16+
| `plugin.json` | Manifest — lets Subtitle Edit list the plugin without launching it. |
17+
| `PluginContract.cs` | The request/response DTOs (the JSON contract). |
18+
| `Program.cs` | Entry point: read request → transform → write response → exit 0. |
19+
| `HaxorTranslator.cs` | The actual text transformation. |
20+
21+
## Build
22+
23+
```
24+
dotnet publish Haxor.csproj -c Release -o publish
25+
```
26+
27+
## Install for testing
28+
29+
Copy the build output plus `plugin.json` into a folder under Subtitle Edit's
30+
`Plugins` directory:
31+
32+
```
33+
Plugins/
34+
Haxor/
35+
plugin.json
36+
Haxor.dll
37+
Haxor.runtimeconfig.json
38+
... (other files from publish/)
39+
```
40+
41+
The manifest uses `"runtime": "dotnet"`, so Subtitle Edit launches it as
42+
`dotnet Haxor.dll <requestFile>` — the .NET 8 runtime must be installed. To ship a
43+
self-contained build instead, publish with `-r <rid> --self-contained` and replace
44+
the `runtime`/`entry` fields in `plugin.json` with an `executables` block.
45+
46+
## Package for the plugin index
47+
48+
Zip the plugin folder (so the zip contains a single top-level `Haxor/` folder),
49+
attach it to a GitHub release, and point `downloadUrl` in `se5-plugins.json` at it.

se5/Haxor/plugin.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"apiVersion": 1,
3+
"name": "Haxor",
4+
"description": "Translates the text of the selected lines (or all lines) to haxor.",
5+
"version": "1.0.0",
6+
"author": "Subtitle Edit",
7+
"url": "https://github.com/SubtitleEdit/plugins/tree/master/se5/Haxor",
8+
"menu": "Translate",
9+
"minSeVersion": "5.0.0",
10+
"runtime": "dotnet",
11+
"entry": "Haxor.dll"
12+
}

0 commit comments

Comments
 (0)