Skip to content

Commit 8de8b26

Browse files
committed
Allow multiple class callouts in one line
1 parent 20a79c9 commit 8de8b26

File tree

6 files changed

+116
-56
lines changed

6 files changed

+116
-56
lines changed

src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class EnhancedCodeBlock(BlockParser parser, ParserContext context)
2020

2121
public int OpeningLength => Info?.Length ?? 0 + 3;
2222

23-
public List<CallOut>? CallOuts { get; set; }
23+
public List<CallOut> CallOuts { get; set; } = [];
2424

2525
public IReadOnlyCollection<CallOut> UniqueCallOuts => CallOuts?.DistinctBy(c => c.Index).ToList() ?? [];
2626

src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private static void RenderCallouts(HtmlRenderer renderer, EnhancedCodeBlock bloc
5555
{
5656
var callOuts = FindCallouts(block.CallOuts ?? [], lineNumber + 1);
5757
foreach (var callOut in callOuts)
58-
renderer.Write($"<span class=\"code-callout\">{callOut.Index}</span>");
58+
renderer.Write($"<span class=\"code-callout\" data-index=\"{callOut.Index}\">{callOut.Index}</span>");
5959
}
6060

6161
private static IEnumerable<CallOut> FindCallouts(

src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockParser.cs

Lines changed: 76 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -98,43 +98,36 @@ public override bool Close(BlockProcessor processor, Block block)
9898
if (codeBlock.OpeningFencedCharCount > 3)
9999
continue;
100100

101-
if (span.IndexOf("<") < 0 && span.IndexOf("//") < 0)
102-
continue;
103-
104-
CallOut? callOut = null;
105-
106-
if (span.IndexOf("<") > 0)
101+
List<CallOut> callOuts = [];
102+
var hasClassicCallout = span.IndexOf("<") > 0;
103+
if (hasClassicCallout)
107104
{
108105
var matchClassicCallout = CallOutParser.CallOutNumber().EnumerateMatches(span);
109-
callOut = EnumerateAnnotations(matchClassicCallout, ref span, ref callOutIndex, originatingLine, false);
110-
}
111-
112-
// only support magic callouts for smaller line lengths
113-
if (callOut is null && span.Length < 200)
106+
callOuts.AddRange(
107+
EnumerateAnnotations(matchClassicCallout, ref span, ref callOutIndex, originatingLine, false)
108+
);
109+
} else if (span.Length < 200)
114110
{
115111
var matchInline = CallOutParser.MathInlineAnnotation().EnumerateMatches(span);
116-
callOut = EnumerateAnnotations(matchInline, ref span, ref callOutIndex, originatingLine,
117-
true);
112+
callOuts.AddRange(
113+
EnumerateAnnotations(matchInline, ref span, ref callOutIndex, originatingLine, true)
114+
);
118115
}
119-
120-
if (callOut is null)
121-
continue;
122-
123-
codeBlock.CallOuts ??= [];
124-
codeBlock.CallOuts.Add(callOut);
116+
codeBlock.CallOuts.AddRange(callOuts);
125117
}
126118

127119
//update string slices to ignore call outs
128-
if (codeBlock.CallOuts is not null)
120+
if (codeBlock.CallOuts?.Count > 0)
129121
{
130-
foreach (var callout in codeBlock.CallOuts)
122+
foreach (var calloutLine in codeBlock.CallOuts.Select(c => c.Line).Distinct())
131123
{
132-
var line = lines.Lines[callout.Line - 1];
133-
134-
var newSpan = line.Slice.AsSpan()[..callout.SliceStart];
124+
var line = lines.Lines[calloutLine - 1];
125+
var index = Math.Max(line.Slice.AsSpan().IndexOf("//"), line.Slice.AsSpan().IndexOf('#'));
126+
if (index < 0)
127+
continue;
128+
var newSpan = line.Slice.AsSpan()[..(index)];
135129
var s = new StringSlice(newSpan.ToString());
136-
lines.Lines[callout.Line - 1] = new StringLine(ref s);
137-
130+
lines.Lines[calloutLine - 1] = new StringLine(ref s);
138131
}
139132
}
140133

@@ -149,44 +142,83 @@ public override bool Close(BlockProcessor processor, Block block)
149142
return base.Close(processor, block);
150143
}
151144

152-
private static CallOut? EnumerateAnnotations(Regex.ValueMatchEnumerator matches,
145+
private static List<CallOut> EnumerateAnnotations(Regex.ValueMatchEnumerator matches,
153146
ref ReadOnlySpan<char> span,
154147
ref int callOutIndex,
155148
int originatingLine,
156149
bool inlineCodeAnnotation)
157150
{
151+
var callOuts = new List<CallOut>();
158152
foreach (var match in matches)
159153
{
160154
if (match.Length == 0)
161155
continue;
162156

163-
var startIndex = span.LastIndexOf("<");
164-
if (!inlineCodeAnnotation && startIndex <= 0)
165-
continue;
166157
if (inlineCodeAnnotation)
167158
{
168-
startIndex = Math.Max(span.LastIndexOf("//"), span.LastIndexOf('#'));
169-
if (startIndex <= 0)
170-
continue;
159+
var callOut = ParseMagicCallout(match, ref span, ref callOutIndex, originatingLine);
160+
if (callOut != null)
161+
return [callOut];
162+
continue;
171163
}
172164

173-
callOutIndex++;
174-
var callout = span.Slice(match.Index + startIndex, match.Length - startIndex);
175-
var index = callOutIndex;
176-
if (!inlineCodeAnnotation && int.TryParse(callout.Trim(['<', '>']), out index))
177-
{
165+
var classicCallOuts = ParseClassicCallOuts(match, ref span, ref callOutIndex, originatingLine);
166+
callOuts.AddRange(classicCallOuts);
167+
}
178168

179-
}
180-
return new CallOut
169+
return callOuts;
170+
}
171+
172+
private static CallOut? ParseMagicCallout(ValueMatch match, ref ReadOnlySpan<char> span, ref int callOutIndex, int originatingLine)
173+
{
174+
var startIndex = Math.Max(span.LastIndexOf("//"), span.LastIndexOf('#'));
175+
if (startIndex <= 0)
176+
return null;
177+
178+
callOutIndex++;
179+
var callout = span.Slice(match.Index + startIndex, match.Length - startIndex);
180+
181+
return new CallOut
182+
{
183+
Index = callOutIndex,
184+
Text = callout.TrimStart('/').TrimStart('#').TrimStart().ToString(),
185+
InlineCodeAnnotation = true,
186+
SliceStart = startIndex,
187+
Line = originatingLine,
188+
};
189+
}
190+
191+
private static List<CallOut> ParseClassicCallOuts(ValueMatch match, ref ReadOnlySpan<char> span, ref int callOutIndex, int originatingLine)
192+
{
193+
var startIndex = span.LastIndexOf("<");
194+
if (startIndex <= 0)
195+
return [];
196+
197+
callOutIndex++;
198+
199+
var allStartIndices = new List<int>();
200+
for (var i = 0; i < span.Length; i++)
201+
{
202+
if (span[i] == '<')
203+
allStartIndices.Add(i);
204+
}
205+
206+
var callOuts = new List<CallOut>();
207+
foreach (var individualStartIndex in allStartIndices)
208+
{
209+
var endIndex = span.Slice(match.Index + individualStartIndex).IndexOf('>') + 1;
210+
var callout = span.Slice(match.Index + individualStartIndex, endIndex);
211+
_ = int.TryParse(callout.Trim(['<', '>']), out var index);
212+
callOuts.Add(new CallOut
181213
{
182214
Index = index,
183215
Text = callout.TrimStart('/').TrimStart('#').TrimStart().ToString(),
184-
InlineCodeAnnotation = inlineCodeAnnotation,
185-
SliceStart = startIndex,
216+
InlineCodeAnnotation = false,
217+
SliceStart = individualStartIndex,
186218
Line = originatingLine,
187-
};
219+
});
188220
}
189221

190-
return null;
222+
return callOuts;
191223
}
192224
}

src/Elastic.Markdown/_static/copybutton.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,14 @@ function escapeRegExp(string) {
153153
* Removes excluded text from a Node.
154154
*
155155
* @param {Node} target Node to filter.
156-
* @param {string} exclude CSS selector of nodes to exclude.
156+
* @param {string[]} excludes CSS selector of nodes to exclude.
157157
* @returns {DOMString} Text from `target` with text removed.
158158
*/
159-
function filterText(target, exclude) {
159+
function filterText(target, excludes) {
160160
const clone = target.cloneNode(true); // clone as to not modify the live DOM
161-
if (exclude) {
162-
// remove excluded nodes
163-
clone.querySelectorAll(exclude).forEach(node => node.remove());
164-
}
161+
excludes.forEach(exclude => {
162+
clone.querySelectorAll(excludes).forEach(node => node.remove());
163+
})
165164
return clone.innerText;
166165
}
167166

@@ -222,11 +221,9 @@ function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onl
222221

223222
var copyTargetText = (trigger) => {
224223
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
225-
226224
// get filtered text
227-
let exclude = '.linenos';
228-
229-
let text = filterText(target, exclude);
225+
let excludes = ['.code-callout', '.linenos'];
226+
let text = filterText(target, excludes);
230227
return formatCopyText(text, '', false, true, true, true, '', '')
231228
}
232229

src/Elastic.Markdown/_static/custom.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ See https://github.com/elastic/docs-builder/issues/219 for further details
150150
justify-content: center;
151151
margin: 0;
152152
transform: translateY(-2px);
153+
user-select: none; /* Standard */
154+
-webkit-user-select: none; /* Safari */
155+
-moz-user-select: none; /* Firefox */
156+
-ms-user-select: none; /* IE10+/Edge */
157+
user-select: none; /* Standard */
158+
}
159+
160+
.yue code span.code-callout:not(:last-child) {
161+
margin-right: 5px;
153162
}
154163

155164
.yue code span.code-callout > span {

tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,25 @@ public void ParsesMagicCallOuts() => Block!.CallOuts
168168
[Fact]
169169
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
170170
}
171+
172+
public class MultipleCalloutsInOneLine(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
173+
"""
174+
var x = 1; <1>
175+
var y = x - 2;
176+
var z = y - 2; <1> <2>
177+
""",
178+
"""
179+
1. First callout
180+
2. Second callout
181+
"""
182+
)
183+
{
184+
[Fact]
185+
public void ParsesMagicCallOuts() => Block!.CallOuts
186+
.Should().NotBeNullOrEmpty()
187+
.And.HaveCount(3)
188+
.And.OnlyContain(c => c.Text.StartsWith("<"));
189+
190+
[Fact]
191+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
192+
}

0 commit comments

Comments
 (0)