Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for code callouts #118

Merged
merged 4 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/source/markup/callout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Callouts
---

You can use the regular markdown code block:

```yaml
project:
title: MyST Markdown
github: https://github.com/jupyter-book/mystmd
license:
code: MIT
content: CC-BY-4.0 <1>
subject: MyST Markdown
```


### C#

```csharp
var apiKey = new ApiKey("<API_KEY>"); // Set up the api key
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
```
137 changes: 122 additions & 15 deletions docs/source/markup/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You can use the regular markdown code block:

```yaml
project:
title: MyST Markdown
title: MyST Markdown
github: https://github.com/jupyter-book/mystmd
license:
code: MIT
Expand All @@ -26,13 +26,10 @@ project:
subject: MyST Markdown
```

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`.
For now we only support the `caption` option on the `{code}` or `{code-block}`

```{code-block} yaml
:linenos:
:caption: How to configure `license` of a project
:name: myst.yml
:emphasize-lines: 4, 5, 6
project:
title: MyST Markdown
github: https://github.com/jupyter-book/mystmd
Expand All @@ -42,15 +39,125 @@ project:
subject: MyST Markdown
```

```{code-block} python
:caption: Code blocks can also have sidebars.
:linenos:
## Code Callouts

### YAML

```yaml
project:
title: MyST Markdown #1
github: https://github.com/jupyter-book/mystmd
license:
code: MIT
content: CC-BY-4.0
subject: MyST Markdown
```

### Java

```java
// Create the low-level client
RestClient restClient = RestClient
.builder(HttpHost.create(serverUrl)) //1
.setDefaultHeaders(new Header[]{
new BasicHeader("Authorization", "ApiKey " + apiKey)
})
.build();
```

### Javascript

```javascript
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
cloud: {
id: '<cloud-id>' //1
},
auth: {
username: 'elastic',
password: 'changeme'
}
})
```

### Ruby

```ruby
require 'elasticsearch'

client = Elasticsearch::Client.new(
cloud_id: '<CloudID>'
user: '<Username>', #1
password: '<Password>',
)
```

### Go

```go
cfg := elasticsearch.Config{
CloudID: "CLOUD_ID", //1
APIKey: "API_KEY"
}
es, err := elasticsearch.NewClient(cfg)
```

### C#

print("one")
print("two")
print("three")
print("four")
print("five")
print("six")
print("seven")
```csharp
var apiKey = new ApiKey("<API_KEY>"); //1
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
```

### PHP

```php
$hosts = [
'192.168.1.1:9200', //1
'192.168.1.2', // Just IP
'mydomain.server.com:9201', // Domain + Port
'mydomain2.server.com', // Just Domain
'https://localhost', // SSL to localhost
'https://192.168.1.3:9200' // SSL to IP + Port
];
$client = ClientBuilder::create() // Instantiate a new ClientBuilder
->setHosts($hosts) // Set the hosts
->build(); // Build the client object
```

### Perl

```perl
my $e = Search::Elasticsearch->new( #1
nodes => [ 'https://my-test.es.us-central1.gcp.cloud.es.io' ],
elastic_cloud_api_key => 'insert here the API Key'
);
```
### Python

```python
from elasticsearch import Elasticsearch

ELASTIC_PASSWORD = "<password>" #1

# Found in the 'Manage Deployment' page
CLOUD_ID = "deployment-name:dXMtZWFzdDQuZ2Nw..."

# Create the client instance
client = Elasticsearch(
cloud_id=CLOUD_ID,
basic_auth=("elastic", ELASTIC_PASSWORD)
)

# Successful response!
client.info()
# {'name': 'instance-0000000000', 'cluster_name': ...}
```
### Rust

```rust
let url = Url::parse("https://example.com")?; //1
let conn_pool = SingleNodeConnectionPool::new(url);
let transport = TransportBuilder::new(conn_pool).disable_proxy().build()?;
let client = Elasticsearch::new(transport);
```
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public static void EmitWarning(this BuildContext context, IFileInfo file, string
context.Collector.Channel.Write(d);
}

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

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

public static void EmitWarning(this DirectiveBlock block, string message)
public static void EmitWarning(this IBlockExtension block, string message)
{
if (block.SkipValidation) return;

Expand All @@ -123,7 +123,7 @@ public static void EmitWarning(this DirectiveBlock block, string message)
File = block.CurrentFile.FullName,
Line = block.Line + 1,
Column = block.Column,
Length = block.Directive.Length + 4,
Length = block.OpeningLength + 4,
Message = message
};
block.Build.Collector.Channel.Write(d);
Expand Down
16 changes: 16 additions & 0 deletions src/Elastic.Markdown/Myst/CodeBlocks/CallOut.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elastic.Markdown.Myst.CodeBlocks;

public record CallOut
{
public required int Index { get; init; }
public required string Text { get; init; }
public required bool InlineCodeAnnotation { get; init; }

public required int SliceStart { get; init; }

public required int Line { get; init; }
}
19 changes: 19 additions & 0 deletions src/Elastic.Markdown/Myst/CodeBlocks/CallOutParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Text.RegularExpressions;

namespace Elastic.Markdown.Myst.CodeBlocks;

public static partial class CallOutParser
{
[GeneratedRegex(@"^.+\S+.*?\s<\d+>$", RegexOptions.IgnoreCase, "en-US")]
public static partial Regex CallOutNumber();

[GeneratedRegex(@"^.+\S+.*?\s(?:\/\/|#)\s[^""]+$", RegexOptions.IgnoreCase, "en-US")]
public static partial Regex MathInlineAnnotation();

[GeneratedRegex(@"\{\{[^\r\n}]+?\}\}", RegexOptions.IgnoreCase, "en-US")]
public static partial Regex MatchSubstitutions();
}
28 changes: 28 additions & 0 deletions src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.IO.Abstractions;
using Elastic.Markdown.Myst.Directives;
using Markdig.Parsers;
using Markdig.Syntax;

namespace Elastic.Markdown.Myst.CodeBlocks;

public class EnhancedCodeBlock(BlockParser parser, ParserContext context)
: FencedCodeBlock(parser), IBlockExtension
{
public BuildContext Build { get; } = context.Build;

public IFileInfo CurrentFile { get; } = context.Path;

public bool SkipValidation { get; } = context.SkipValidation;

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

public List<CallOut>? CallOuts { get; set; }

public bool InlineAnnotations { get; set; }

public string Language { get; set; } = "unknown";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Myst.Directives;
using Elastic.Markdown.Slices.Directives;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using RazorSlices;

namespace Elastic.Markdown.Myst.CodeBlocks;

public class EnhancedCodeBlockHtmlRenderer : HtmlObjectRenderer<EnhancedCodeBlock>
{

private static void RenderRazorSlice<T>(RazorSlice<T> slice, HtmlRenderer renderer, EnhancedCodeBlock block)
{
var html = slice.RenderAsync().GetAwaiter().GetResult();
var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries);
renderer.Write(blocks[0]);
renderer.WriteLeafRawLines(block, true, false, false);
renderer.Write(blocks[1]);
}
protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block)
{
var callOuts = block.CallOuts ?? [];

var slice = Code.Create(new CodeViewModel
{
CrossReferenceName = string.Empty,// block.CrossReferenceName,
Language = block.Language,
Caption = string.Empty
});

RenderRazorSlice(slice, renderer, block);

if (!block.InlineAnnotations && callOuts.Count > 0)
{
var index = block.Parent!.IndexOf(block);
if (index == block.Parent!.Count - 1)
block.EmitError("Code block with annotations is not followed by any content, needs numbered list");
else
{
var siblingBlock = block.Parent[index + 1];
if (siblingBlock is not ListBlock)
block.EmitError("Code block with annotations is not followed by a list");
if (siblingBlock is ListBlock l && l.Count != callOuts.Count)
{
block.EmitError(
$"Code block has {callOuts.Count} callouts but the following list only has {l.Count}");
}
else if (siblingBlock is ListBlock listBlock)
{
block.Parent.Remove(listBlock);
renderer.WriteLine("<ol class=\"code-callouts\">");
foreach (var child in listBlock)
{
var listItem = (ListItemBlock)child;
var previousImplicit = renderer.ImplicitParagraph;
renderer.ImplicitParagraph = !listBlock.IsLoose;

renderer.EnsureLine();
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<li");
renderer.WriteAttributes(listItem);
renderer.Write('>');
}

renderer.WriteChildren(listItem);

if (renderer.EnableHtmlForBlock)
renderer.WriteLine("</li>");

renderer.EnsureLine();
renderer.ImplicitParagraph = previousImplicit;
}
renderer.WriteLine("</ol>");
}
}
}
else if (block.InlineAnnotations)
{
renderer.WriteLine("<ol class=\"code-callouts\">");
foreach (var c in block.CallOuts ?? [])
{
renderer.WriteLine("<li>");
renderer.WriteLine(c.Text);
renderer.WriteLine("</li>");
}

renderer.WriteLine("</ol>");
}
}
}
Loading
Loading