File size: 3,228 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using Repoverse.Core.Model;

namespace Repoverse.Core;

/// <summary>
/// Deep link into the world (§76). Grammar, version 1:
/// <code>
/// repoverse://{owner}/{repo}[/{path}][#L{line}]
/// repoverse://{owner}/{repo}?room={id}
/// repoverse://@{x},{y},{z}
/// </code>
/// Path segments are percent-encoded; the repo part matches GitHub's owner/name.
/// </summary>
public sealed record RepoverseUri
{
    public const string Scheme = "repoverse";

    public RepoId? Repo { get; init; }
    public string? Path { get; init; }
    public int? Line { get; init; }
    public int? Room { get; init; }
    public (int X, int Y, int Z)? Location { get; init; }

    public static RepoverseUri Parse(string text)
    {
        const string prefix = Scheme + "://";
        if (!text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            throw new FormatException($"Not a {Scheme} URI: '{text}'.");
        var rest = text[prefix.Length..];

        if (rest.StartsWith('@'))
        {
            var p = rest[1..].Split(',');
            if (p.Length != 3) throw new FormatException("Location must be @x,y,z.");
            return new RepoverseUri { Location = (int.Parse(p[0]), int.Parse(p[1]), int.Parse(p[2])) };
        }

        int? line = null, room = null;
        var hash = rest.IndexOf('#');
        if (hash >= 0)
        {
            var frag = rest[(hash + 1)..];
            if (!frag.StartsWith('L') || !int.TryParse(frag[1..], out var l)) throw new FormatException($"Bad fragment '#{frag}'.");
            line = l;
            rest = rest[..hash];
        }
        var q = rest.IndexOf('?');
        if (q >= 0)
        {
            foreach (var kv in rest[(q + 1)..].Split('&'))
                if (kv.StartsWith("room=") && int.TryParse(kv[5..], out var r)) room = r;
                else throw new FormatException($"Unknown query '{kv}'.");
            rest = rest[..q];
        }

        var parts = rest.Split('/', 3);
        if (parts.Length < 2 || parts[0].Length == 0 || parts[1].Length == 0) throw new FormatException("Expected owner/repo.");
        var path = parts.Length == 3 && parts[2].Length > 0
            ? string.Join('/', parts[2].Split('/').Select(Uri.UnescapeDataString))
            : null;
        return new RepoverseUri { Repo = new RepoId(Uri.UnescapeDataString(parts[0]), Uri.UnescapeDataString(parts[1])), Path = path, Line = line, Room = room };
    }

    public override string ToString()
    {
        if (Location is var (x, y, z)) return $"{Scheme}://@{x},{y},{z}";
        var s = $"{Scheme}://{Uri.EscapeDataString(Repo!.Value.Owner)}/{Uri.EscapeDataString(Repo.Value.Name)}";
        if (Path != null) s += "/" + string.Join('/', Path.Split('/').Select(Uri.EscapeDataString));
        if (Room != null) s += $"?room={Room}";
        if (Line != null) s += $"#L{Line}";
        return s;
    }

    /// <summary>External reference back to GitHub (§77); internal identity stays the RepoId.</summary>
    public string? GitHubUrl(string branch = "HEAD") => Repo is not { } r ? null
        : Path == null ? $"https://github.com/{r.Owner}/{r.Name}"
        : $"https://github.com/{r.Owner}/{r.Name}/blob/{branch}/{Path}{(Line is { } l ? $"#L{l}" : "")}";
}