Skip to content

Commit

Permalink
Add support for code callouts (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mpdreamz authored Dec 19, 2024
1 parent 42cc222 commit 6ddd7a6
Show file tree
Hide file tree
Showing 21 changed files with 780 additions and 81 deletions.
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

Check failure on line 7 in docs/source/markup/callout.md

View workflow job for this annotation

GitHub Actions / deploy

Code block with annotations is not followed by a list
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

0 comments on commit 6ddd7a6

Please sign in to comment.