Skip to content

Commit 6ddd7a6

Browse files
authored
Add support for code callouts (#118)
1 parent 42cc222 commit 6ddd7a6

21 files changed

+780
-81
lines changed

docs/source/markup/callout.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: Callouts
3+
---
4+
5+
You can use the regular markdown code block:
6+
7+
```yaml
8+
project:
9+
title: MyST Markdown
10+
github: https://github.com/jupyter-book/mystmd
11+
license:
12+
code: MIT
13+
content: CC-BY-4.0 <1>
14+
subject: MyST Markdown
15+
```
16+
17+
18+
### C#
19+
20+
```csharp
21+
var apiKey = new ApiKey("<API_KEY>"); // Set up the api key
22+
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
23+
```

docs/source/markup/code.md

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ You can use the regular markdown code block:
66

77
```yaml
88
project:
9-
title: MyST Markdown
9+
title: MyST Markdown
1010
github: https://github.com/jupyter-book/mystmd
1111
license:
1212
code: MIT
@@ -26,13 +26,10 @@ project:
2626
subject: MyST Markdown
2727
```
2828
29-
This page also documents the [code directive](https://mystmd.org/guide/directives). It mentions `code-block` and `sourcecode` as aliases of the `code` directive. But `code-block` seems to behave differently. For example the `caption` option works for `code-block`, but not for `code`.
29+
For now we only support the `caption` option on the `{code}` or `{code-block}`
3030

3131
```{code-block} yaml
32-
:linenos:
3332
:caption: How to configure `license` of a project
34-
:name: myst.yml
35-
:emphasize-lines: 4, 5, 6
3633
project:
3734
title: MyST Markdown
3835
github: https://github.com/jupyter-book/mystmd
@@ -42,15 +39,125 @@ project:
4239
subject: MyST Markdown
4340
```
4441
45-
```{code-block} python
46-
:caption: Code blocks can also have sidebars.
47-
:linenos:
42+
## Code Callouts
43+
44+
### YAML
45+
46+
```yaml
47+
project:
48+
title: MyST Markdown #1
49+
github: https://github.com/jupyter-book/mystmd
50+
license:
51+
code: MIT
52+
content: CC-BY-4.0
53+
subject: MyST Markdown
54+
```
55+
56+
### Java
57+
58+
```java
59+
// Create the low-level client
60+
RestClient restClient = RestClient
61+
.builder(HttpHost.create(serverUrl)) //1
62+
.setDefaultHeaders(new Header[]{
63+
new BasicHeader("Authorization", "ApiKey " + apiKey)
64+
})
65+
.build();
66+
```
67+
68+
### Javascript
69+
70+
```javascript
71+
const { Client } = require('@elastic/elasticsearch')
72+
const client = new Client({
73+
cloud: {
74+
id: '<cloud-id>' //1
75+
},
76+
auth: {
77+
username: 'elastic',
78+
password: 'changeme'
79+
}
80+
})
81+
```
82+
83+
### Ruby
84+
85+
```ruby
86+
require 'elasticsearch'
87+
88+
client = Elasticsearch::Client.new(
89+
cloud_id: '<CloudID>'
90+
user: '<Username>', #1
91+
password: '<Password>',
92+
)
93+
```
94+
95+
### Go
96+
97+
```go
98+
cfg := elasticsearch.Config{
99+
CloudID: "CLOUD_ID", //1
100+
APIKey: "API_KEY"
101+
}
102+
es, err := elasticsearch.NewClient(cfg)
103+
```
104+
105+
### C#
48106

49-
print("one")
50-
print("two")
51-
print("three")
52-
print("four")
53-
print("five")
54-
print("six")
55-
print("seven")
107+
```csharp
108+
var apiKey = new ApiKey("<API_KEY>"); //1
109+
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
56110
```
111+
112+
### PHP
113+
114+
```php
115+
$hosts = [
116+
'192.168.1.1:9200', //1
117+
'192.168.1.2', // Just IP
118+
'mydomain.server.com:9201', // Domain + Port
119+
'mydomain2.server.com', // Just Domain
120+
'https://localhost', // SSL to localhost
121+
'https://192.168.1.3:9200' // SSL to IP + Port
122+
];
123+
$client = ClientBuilder::create() // Instantiate a new ClientBuilder
124+
->setHosts($hosts) // Set the hosts
125+
->build(); // Build the client object
126+
```
127+
128+
### Perl
129+
130+
```perl
131+
my $e = Search::Elasticsearch->new( #1
132+
nodes => [ 'https://my-test.es.us-central1.gcp.cloud.es.io' ],
133+
elastic_cloud_api_key => 'insert here the API Key'
134+
);
135+
```
136+
### Python
137+
138+
```python
139+
from elasticsearch import Elasticsearch
140+
141+
ELASTIC_PASSWORD = "<password>" #1
142+
143+
# Found in the 'Manage Deployment' page
144+
CLOUD_ID = "deployment-name:dXMtZWFzdDQuZ2Nw..."
145+
146+
# Create the client instance
147+
client = Elasticsearch(
148+
cloud_id=CLOUD_ID,
149+
basic_auth=("elastic", ELASTIC_PASSWORD)
150+
)
151+
152+
# Successful response!
153+
client.info()
154+
# {'name': 'instance-0000000000', 'cluster_name': ...}
155+
```
156+
### Rust
157+
158+
```rust
159+
let url = Url::parse("https://example.com")?; //1
160+
let conn_pool = SingleNodeConnectionPool::new(url);
161+
let transport = TransportBuilder::new(conn_pool).disable_proxy().build()?;
162+
let client = Elasticsearch::new(transport);
163+
```

src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public static void EmitWarning(this BuildContext context, IFileInfo file, string
9797
context.Collector.Channel.Write(d);
9898
}
9999

100-
public static void EmitError(this DirectiveBlock block, string message, Exception? e = null)
100+
public static void EmitError(this IBlockExtension block, string message, Exception? e = null)
101101
{
102102
if (block.SkipValidation) return;
103103

@@ -107,13 +107,13 @@ public static void EmitError(this DirectiveBlock block, string message, Exceptio
107107
File = block.CurrentFile.FullName,
108108
Line = block.Line + 1,
109109
Column = block.Column,
110-
Length = block.Directive.Length + 5,
110+
Length = block.OpeningLength + 5,
111111
Message = message + (e != null ? Environment.NewLine + e : string.Empty),
112112
};
113113
block.Build.Collector.Channel.Write(d);
114114
}
115115

116-
public static void EmitWarning(this DirectiveBlock block, string message)
116+
public static void EmitWarning(this IBlockExtension block, string message)
117117
{
118118
if (block.SkipValidation) return;
119119

@@ -123,7 +123,7 @@ public static void EmitWarning(this DirectiveBlock block, string message)
123123
File = block.CurrentFile.FullName,
124124
Line = block.Line + 1,
125125
Column = block.Column,
126-
Length = block.Directive.Length + 4,
126+
Length = block.OpeningLength + 4,
127127
Message = message
128128
};
129129
block.Build.Collector.Channel.Write(d);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace Elastic.Markdown.Myst.CodeBlocks;
6+
7+
public record CallOut
8+
{
9+
public required int Index { get; init; }
10+
public required string Text { get; init; }
11+
public required bool InlineCodeAnnotation { get; init; }
12+
13+
public required int SliceStart { get; init; }
14+
15+
public required int Line { get; init; }
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Text.RegularExpressions;
6+
7+
namespace Elastic.Markdown.Myst.CodeBlocks;
8+
9+
public static partial class CallOutParser
10+
{
11+
[GeneratedRegex(@"^.+\S+.*?\s<\d+>$", RegexOptions.IgnoreCase, "en-US")]
12+
public static partial Regex CallOutNumber();
13+
14+
[GeneratedRegex(@"^.+\S+.*?\s(?:\/\/|#)\s[^""]+$", RegexOptions.IgnoreCase, "en-US")]
15+
public static partial Regex MathInlineAnnotation();
16+
17+
[GeneratedRegex(@"\{\{[^\r\n}]+?\}\}", RegexOptions.IgnoreCase, "en-US")]
18+
public static partial Regex MatchSubstitutions();
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.IO.Abstractions;
6+
using Elastic.Markdown.Myst.Directives;
7+
using Markdig.Parsers;
8+
using Markdig.Syntax;
9+
10+
namespace Elastic.Markdown.Myst.CodeBlocks;
11+
12+
public class EnhancedCodeBlock(BlockParser parser, ParserContext context)
13+
: FencedCodeBlock(parser), IBlockExtension
14+
{
15+
public BuildContext Build { get; } = context.Build;
16+
17+
public IFileInfo CurrentFile { get; } = context.Path;
18+
19+
public bool SkipValidation { get; } = context.SkipValidation;
20+
21+
public int OpeningLength => Info?.Length ?? 0 + 3;
22+
23+
public List<CallOut>? CallOuts { get; set; }
24+
25+
public bool InlineAnnotations { get; set; }
26+
27+
public string Language { get; set; } = "unknown";
28+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Diagnostics;
6+
using Elastic.Markdown.Myst.Directives;
7+
using Elastic.Markdown.Slices.Directives;
8+
using Markdig.Renderers;
9+
using Markdig.Renderers.Html;
10+
using Markdig.Syntax;
11+
using RazorSlices;
12+
13+
namespace Elastic.Markdown.Myst.CodeBlocks;
14+
15+
public class EnhancedCodeBlockHtmlRenderer : HtmlObjectRenderer<EnhancedCodeBlock>
16+
{
17+
18+
private static void RenderRazorSlice<T>(RazorSlice<T> slice, HtmlRenderer renderer, EnhancedCodeBlock block)
19+
{
20+
var html = slice.RenderAsync().GetAwaiter().GetResult();
21+
var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries);
22+
renderer.Write(blocks[0]);
23+
renderer.WriteLeafRawLines(block, true, false, false);
24+
renderer.Write(blocks[1]);
25+
}
26+
protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block)
27+
{
28+
var callOuts = block.CallOuts ?? [];
29+
30+
var slice = Code.Create(new CodeViewModel
31+
{
32+
CrossReferenceName = string.Empty,// block.CrossReferenceName,
33+
Language = block.Language,
34+
Caption = string.Empty
35+
});
36+
37+
RenderRazorSlice(slice, renderer, block);
38+
39+
if (!block.InlineAnnotations && callOuts.Count > 0)
40+
{
41+
var index = block.Parent!.IndexOf(block);
42+
if (index == block.Parent!.Count - 1)
43+
block.EmitError("Code block with annotations is not followed by any content, needs numbered list");
44+
else
45+
{
46+
var siblingBlock = block.Parent[index + 1];
47+
if (siblingBlock is not ListBlock)
48+
block.EmitError("Code block with annotations is not followed by a list");
49+
if (siblingBlock is ListBlock l && l.Count != callOuts.Count)
50+
{
51+
block.EmitError(
52+
$"Code block has {callOuts.Count} callouts but the following list only has {l.Count}");
53+
}
54+
else if (siblingBlock is ListBlock listBlock)
55+
{
56+
block.Parent.Remove(listBlock);
57+
renderer.WriteLine("<ol class=\"code-callouts\">");
58+
foreach (var child in listBlock)
59+
{
60+
var listItem = (ListItemBlock)child;
61+
var previousImplicit = renderer.ImplicitParagraph;
62+
renderer.ImplicitParagraph = !listBlock.IsLoose;
63+
64+
renderer.EnsureLine();
65+
if (renderer.EnableHtmlForBlock)
66+
{
67+
renderer.Write("<li");
68+
renderer.WriteAttributes(listItem);
69+
renderer.Write('>');
70+
}
71+
72+
renderer.WriteChildren(listItem);
73+
74+
if (renderer.EnableHtmlForBlock)
75+
renderer.WriteLine("</li>");
76+
77+
renderer.EnsureLine();
78+
renderer.ImplicitParagraph = previousImplicit;
79+
}
80+
renderer.WriteLine("</ol>");
81+
}
82+
}
83+
}
84+
else if (block.InlineAnnotations)
85+
{
86+
renderer.WriteLine("<ol class=\"code-callouts\">");
87+
foreach (var c in block.CallOuts ?? [])
88+
{
89+
renderer.WriteLine("<li>");
90+
renderer.WriteLine(c.Text);
91+
renderer.WriteLine("</li>");
92+
}
93+
94+
renderer.WriteLine("</ol>");
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)