Skip to content

Commit 3fa6a22

Browse files
committed
Allow multiple class callouts in one line
1 parent 20a79c9 commit 3fa6a22

File tree

7 files changed

+201
-51
lines changed

7 files changed

+201
-51
lines changed

docs/index.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,32 @@ Elastic Docs V3 is our next-generation documentation platform designed to improv
99
* [Configure content sets in V3](./configure/index.md)
1010
* [Learn about V3 syntax](./syntax/index.md)
1111
* [Contribute to V3 (developer guide)](./development/index.md)
12+
13+
14+
:::{dropdown} Markdown
15+
:open:
16+
17+
````markdown
18+
```csharp
19+
var x = 1; <1>
20+
var y = x - 2;
21+
var z = y - 2; <1> <2>
22+
```
23+
24+
1. Foo
25+
2. Bar
26+
````
27+
28+
:::
29+
30+
:::{dropdown} Output
31+
:open:
32+
```csharp
33+
var x = 1; <1>
34+
var y = x - 2;
35+
var z = y - 2; <1> <2>
36+
```
37+
38+
1. Foo
39+
2. Bar
40+
:::

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: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,39 +98,48 @@ 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;
101+
List<CallOut> callOuts = [];
105102

106-
if (span.IndexOf("<") > 0)
103+
var hasClassicCallout = span.IndexOf("<") > 0;
104+
if (hasClassicCallout)
107105
{
108106
var matchClassicCallout = CallOutParser.CallOutNumber().EnumerateMatches(span);
109-
callOut = EnumerateAnnotations(matchClassicCallout, ref span, ref callOutIndex, originatingLine, false);
107+
callOuts.AddRange(
108+
EnumerateAnnotations(matchClassicCallout, ref span, ref callOutIndex, originatingLine, false)
109+
);
110110
}
111111

112112
// only support magic callouts for smaller line lengths
113-
if (callOut is null && span.Length < 200)
113+
if (callOuts.Count == 0 && span.Length < 200)
114114
{
115115
var matchInline = CallOutParser.MathInlineAnnotation().EnumerateMatches(span);
116-
callOut = EnumerateAnnotations(matchInline, ref span, ref callOutIndex, originatingLine,
117-
true);
116+
callOuts.AddRange(
117+
EnumerateAnnotations(matchInline, ref span, ref callOutIndex, originatingLine, true)
118+
);
118119
}
119120

120-
if (callOut is null)
121+
if (callOuts.Count == 0)
121122
continue;
122123

123124
codeBlock.CallOuts ??= [];
124-
codeBlock.CallOuts.Add(callOut);
125+
codeBlock.CallOuts.AddRange(callOuts);
125126
}
126127

127128
//update string slices to ignore call outs
128-
if (codeBlock.CallOuts is not null)
129+
if (codeBlock.CallOuts?.Count > 0)
129130
{
130-
foreach (var callout in codeBlock.CallOuts)
131+
132+
var callouts = codeBlock.CallOuts.Aggregate(new Dictionary<int, CallOut>(), (acc, curr) =>
131133
{
132-
var line = lines.Lines[callout.Line - 1];
134+
if (acc.TryAdd(curr.Line, curr)) return acc;
135+
if (acc[curr.Line].SliceStart > curr.SliceStart)
136+
acc[curr.Line] = curr;
137+
return acc;
138+
});
133139

140+
foreach (var callout in callouts.Values)
141+
{
142+
var line = lines.Lines[callout.Line - 1];
134143
var newSpan = line.Slice.AsSpan()[..callout.SliceStart];
135144
var s = new StringSlice(newSpan.ToString());
136145
lines.Lines[callout.Line - 1] = new StringLine(ref s);
@@ -149,44 +158,79 @@ public override bool Close(BlockProcessor processor, Block block)
149158
return base.Close(processor, block);
150159
}
151160

152-
private static CallOut? EnumerateAnnotations(Regex.ValueMatchEnumerator matches,
161+
private static List<CallOut> EnumerateAnnotations(Regex.ValueMatchEnumerator matches,
153162
ref ReadOnlySpan<char> span,
154163
ref int callOutIndex,
155164
int originatingLine,
156165
bool inlineCodeAnnotation)
157166
{
167+
var callOuts = new List<CallOut>();
158168
foreach (var match in matches)
159169
{
160170
if (match.Length == 0)
161171
continue;
162172

163-
var startIndex = span.LastIndexOf("<");
164-
if (!inlineCodeAnnotation && startIndex <= 0)
165-
continue;
166173
if (inlineCodeAnnotation)
167174
{
168-
startIndex = Math.Max(span.LastIndexOf("//"), span.LastIndexOf('#'));
169-
if (startIndex <= 0)
170-
continue;
175+
var callOut = ParseMagicCallout(match, ref span, ref callOutIndex, originatingLine);
176+
if (callOut != null)
177+
return [callOut];
178+
continue;
171179
}
172180

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-
{
181+
var classicCallOuts = ParseClassicCallOuts(match, ref span, ref callOutIndex, originatingLine);
182+
callOuts.AddRange(classicCallOuts);
183+
}
178184

179-
}
180-
return new CallOut
185+
return callOuts;
186+
}
187+
188+
private static CallOut? ParseMagicCallout(ValueMatch match, ref ReadOnlySpan<char> span, ref int callOutIndex, int originatingLine)
189+
{
190+
var startIndex = Math.Max(span.LastIndexOf("//"), span.LastIndexOf('#'));
191+
if (startIndex <= 0)
192+
return null;
193+
194+
callOutIndex++;
195+
var callout = span.Slice(match.Index + startIndex, match.Length - startIndex);
196+
197+
return new CallOut
198+
{
199+
Index = callOutIndex,
200+
Text = callout.TrimStart('/').TrimStart('#').TrimStart().ToString(),
201+
InlineCodeAnnotation = true,
202+
SliceStart = startIndex,
203+
Line = originatingLine,
204+
};
205+
}
206+
207+
private static List<CallOut> ParseClassicCallOuts(ValueMatch match, ref ReadOnlySpan<char> span, ref int callOutIndex, int originatingLine)
208+
{
209+
var startIndex = span.LastIndexOf("<");
210+
if (startIndex <= 0)
211+
return [];
212+
var allStartIndices = new List<int>();
213+
for (var i = 0; i < span.Length; i++)
214+
{
215+
if (span[i] == '<')
216+
allStartIndices.Add(i);
217+
}
218+
var callOuts = new List<CallOut>();
219+
foreach (var individualStartIndex in allStartIndices)
220+
{
221+
callOutIndex++;
222+
var endIndex = span.Slice(match.Index + individualStartIndex).IndexOf('>') + 1;
223+
var callout = span.Slice(match.Index + individualStartIndex, endIndex);
224+
_ = int.TryParse(callout.Trim(['<', '>']), out var index);
225+
callOuts.Add(new CallOut
181226
{
182227
Index = index,
183228
Text = callout.TrimStart('/').TrimStart('#').TrimStart().ToString(),
184-
InlineCodeAnnotation = inlineCodeAnnotation,
185-
SliceStart = startIndex,
229+
InlineCodeAnnotation = false,
230+
SliceStart = individualStartIndex,
186231
Line = originatingLine,
187-
};
232+
});
188233
}
189-
190-
return null;
234+
return callOuts;
191235
}
192236
}

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: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,21 +148,92 @@ public void ParsesAllForLineInformation() => Block!.CallOuts
148148

149149
public class ClassicCallOutWithTheRightListItems(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
150150
"""
151-
var x = 1; <1>
152-
var y = x - 2;
153-
var z = y - 2; <2>
151+
receivers: <1>
152+
# ...
153+
otlp:
154+
protocols:
155+
grpc:
156+
endpoint: 0.0.0.0:4317
157+
http:
158+
endpoint: 0.0.0.0:4318
159+
processors: <2>
160+
# ...
161+
memory_limiter:
162+
check_interval: 1s
163+
limit_mib: 2000
164+
batch:
165+
166+
exporters:
167+
debug:
168+
verbosity: detailed <3>
169+
otlp: <4>
170+
# Elastic APM server https endpoint without the "https://" prefix
171+
endpoint: "${env:ELASTIC_APM_SERVER_ENDPOINT}" <5> <7>
172+
headers:
173+
# Elastic APM Server secret token
174+
Authorization: "Bearer ${env:ELASTIC_APM_SECRET_TOKEN}" <6> <7>
175+
176+
service:
177+
pipelines:
178+
traces:
179+
receivers: [otlp]
180+
processors: [..., memory_limiter, batch]
181+
exporters: [debug, otlp]
182+
metrics:
183+
receivers: [otlp]
184+
processors: [..., memory_limiter, batch]
185+
exporters: [debug, otlp]
186+
logs: <8>
187+
receivers: [otlp]
188+
processors: [..., memory_limiter, batch]
189+
exporters: [debug, otlp]
154190
""",
155191
"""
156-
1. First callout
157-
2. Second callout
192+
1. The receivers, like the OTLP receiver, that forward data emitted by APM agents, or the host metrics receiver.
193+
2. We recommend using the Batch processor and the memory limiter processor. For more information, see recommended processors.
194+
3. The debug exporter is helpful for troubleshooting, and supports configurable verbosity levels: basic (default), normal, and detailed.
195+
4. Elastic {observability} endpoint configuration. APM Server supports a ProtoBuf payload via both the OTLP protocol over gRPC transport (OTLP/gRPC) and the OTLP protocol over HTTP transport (OTLP/HTTP). To learn more about these exporters, see the OpenTelemetry Collector documentation: OTLP/HTTP Exporter or OTLP/gRPC exporter. When adding an endpoint to an existing configuration an optional name component can be added, like otlp/elastic, to distinguish endpoints as described in the OpenTelemetry Collector Configuration Basics.
196+
5. Hostname and port of the APM Server endpoint. For example, elastic-apm-server:8200.
197+
6. Credential for Elastic APM secret token authorization (Authorization: "Bearer a_secret_token") or API key authorization (Authorization: "ApiKey an_api_key").
198+
7. Environment-specific configuration parameters can be conveniently passed in as environment variables documented here (e.g. ELASTIC_APM_SERVER_ENDPOINT and ELASTIC_APM_SECRET_TOKEN).
199+
8. [preview] To send OpenTelemetry logs to {stack} version 8.0+, declare a logs pipeline.
158200
"""
159201

160202
)
203+
{
204+
[Fact]
205+
public void ParsesClassicCallouts()
206+
{
207+
Block!.CallOuts
208+
.Should().NotBeNullOrEmpty()
209+
.And.HaveCount(9)
210+
.And.OnlyContain(c => c.Text.StartsWith("<"));
211+
212+
Block!.UniqueCallOuts
213+
.Should().NotBeNullOrEmpty()
214+
.And.HaveCount(8);
215+
}
216+
217+
[Fact]
218+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
219+
}
220+
221+
public class MultipleCalloutsInOneLine(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
222+
"""
223+
var x = 1; // <1>
224+
var y = x - 2;
225+
var z = y - 2; // <1> <2>
226+
""",
227+
"""
228+
1. First callout
229+
2. Second callout
230+
"""
231+
)
161232
{
162233
[Fact]
163234
public void ParsesMagicCallOuts() => Block!.CallOuts
164235
.Should().NotBeNullOrEmpty()
165-
.And.HaveCount(2)
236+
.And.HaveCount(3)
166237
.And.OnlyContain(c => c.Text.StartsWith("<"));
167238

168239
[Fact]

0 commit comments

Comments
 (0)