Forge
csharp4405de34
1#nullable enable
2
3using System.Text.RegularExpressions;
4using CascadeIDE.Models.Intercom;
5
6namespace CascadeIDE.Services.Intercom;
7
8/// <summary>Inline-метки attach в теле сообщения: <c>⟦a:{id}⟧</c> (ADR 0128).</summary>
9public static class IntercomAttachmentMarkers
10{
11 public const char MarkerOpen = '\u27E6';
12 public const char MarkerClose = '\u27E7';
13
14 private static readonly Regex MarkerRegex = new(
15 @"⟦a:(?<id>[0-9a-f]{8})⟧",
16 RegexOptions.CultureInvariant | RegexOptions.Compiled);
17
18 private static readonly Regex BracketRegex = new(
19 @"\[(?<inner>[^\[\]]+)\]",
20 RegexOptions.CultureInvariant | RegexOptions.Compiled);
21
22 public static string FormatMarker(string shortId) => $"{MarkerOpen}a:{shortId}{MarkerClose}";
23
24 public static string NewShortId() => Guid.NewGuid().ToString("N")[..8];
25
26 public static string FormatDisplayLabel(string label) => $"【{label}】";
27
28 /// <summary>Текст кликабельной строки в ленте Intercom (подчёркнутый <c>[label]</c>).</summary>
29 public static string FormatFeedLinkLabel(string label) => $"[{label}]";
30
31 public static string ReplaceMarkersForDisplay(string content, IReadOnlyList<AttachmentAnchor> attachments)
32 {
33 if (string.IsNullOrEmpty(content))
34 return content ?? "";
35
36 var byId = attachments
37 .Where(a => !string.IsNullOrWhiteSpace(a.Id))
38 .ToDictionary(a => a.Id!, StringComparer.OrdinalIgnoreCase);
39
40 return MarkerRegex.Replace(content, m =>
41 {
42 var id = m.Groups["id"].Value;
43 if (!byId.TryGetValue(id, out var anchor))
44 return m.Value;
45 var label = anchor.DisplayLabel;
46 return string.IsNullOrWhiteSpace(label) ? m.Value : FormatDisplayLabel(label);
47 });
48 }
49
50 /// <summary>Разбить prose с wire-маркерами <c>⟦a:…⟧</c> на сегменты; текст attach — displayLabel.</summary>
51 public static IReadOnlyList<IntercomAttachmentFeedSegment> SplitFeedSegments(
52 string contentWithMarkers,
53 IReadOnlyList<AttachmentAnchor> attachments)
54 {
55 if (string.IsNullOrEmpty(contentWithMarkers))
56 return [new IntercomAttachmentFeedSegment(IntercomAttachmentFeedSegmentKind.Prose, "")];
57
58 var byId = attachments
59 .Where(a => !string.IsNullOrWhiteSpace(a.Id))
60 .ToDictionary(a => a.Id!, StringComparer.OrdinalIgnoreCase);
61
62 var list = new List<IntercomAttachmentFeedSegment>();
63 var last = 0;
64 foreach (Match m in MarkerRegex.Matches(contentWithMarkers))
65 {
66 if (m.Index > last)
67 {
68 var prose = contentWithMarkers[last..m.Index];
69 if (prose.Length > 0)
70 list.Add(new IntercomAttachmentFeedSegment(IntercomAttachmentFeedSegmentKind.Prose, prose));
71 }
72
73 var id = m.Groups["id"].Value;
74 byId.TryGetValue(id, out var anchor);
75 var label = anchor?.DisplayLabel ?? id;
76 list.Add(new IntercomAttachmentFeedSegment(
77 IntercomAttachmentFeedSegmentKind.Attachment,
78 FormatFeedLinkLabel(label),
79 anchor,
80 MarkerShortId: id));
81 last = m.Index + m.Length;
82 }
83
84 if (last < contentWithMarkers.Length)
85 list.Add(new IntercomAttachmentFeedSegment(IntercomAttachmentFeedSegmentKind.Prose, contentWithMarkers[last..]));
86
87 return list.Count == 0
88 ? [new IntercomAttachmentFeedSegment(IntercomAttachmentFeedSegmentKind.Prose, contentWithMarkers)]
89 : list;
90 }
91
92 public static void UpdateProseOffsets(string content, IList<AttachmentAnchor> anchors)
93 {
94 if (anchors.Count == 0)
95 return;
96
97 var byId = anchors
98 .Where(a => !string.IsNullOrWhiteSpace(a.Id))
99 .Select((a, i) => (a, i))
100 .ToDictionary(x => x.a.Id!, x => x.i, StringComparer.OrdinalIgnoreCase);
101
102 foreach (Match m in MarkerRegex.Matches(content))
103 {
104 var id = m.Groups["id"].Value;
105 if (!byId.TryGetValue(id, out var idx))
106 continue;
107 var a = anchors[idx];
108 anchors[idx] = a with
109 {
110 ProseStart = m.Index,
111 ProseLength = m.Length,
112 };
113 }
114 }
115
116 public static bool TryExtractBracketSpans(string prose, out List<(int Start, int Length, string Inner)> spans)
117 {
118 spans = [];
119 foreach (Match m in BracketRegex.Matches(prose))
120 {
121 if (m.Groups["inner"].Value.Contains('`', StringComparison.Ordinal))
122 continue;
123 spans.Add((m.Index, m.Length, m.Groups["inner"].Value));
124 }
125
126 return spans.Count > 0;
127 }
128}
129
130public enum IntercomAttachmentFeedSegmentKind
131{
132 Prose = 0,
133 Attachment = 1,
134}
135
136public readonly record struct IntercomAttachmentFeedSegment(
137 IntercomAttachmentFeedSegmentKind Kind,
138 string Text,
139 AttachmentAnchor? Anchor = null,
140 string? MarkerShortId = null);
141
View only · write via MCP/CIDE