diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs
index 92a747e2e..cafabb9b6 100644
--- a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs
+++ b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs
@@ -44,12 +44,25 @@ private static void RenderCodeBlockLines(HtmlRenderer renderer, EnhancedCodeBloc
}
}
- private static void RenderCodeBlockLine(HtmlRenderer renderer, EnhancedCodeBlock block, StringSlice slice, int i)
+ private static void RenderCodeBlockLine(HtmlRenderer renderer, EnhancedCodeBlock block, StringSlice slice, int lineNumber)
{
renderer.WriteEscape(slice);
+ RenderCallouts(renderer, block, lineNumber);
renderer.WriteLine();
}
+ private static void RenderCallouts(HtmlRenderer renderer, EnhancedCodeBlock block, int lineNumber)
+ {
+ var callOuts = FindCallouts(block.CallOuts ?? [], lineNumber + 1);
+ foreach (var callOut in callOuts)
+ renderer.Write($"{callOut.Index}");
+ }
+
+ private static IEnumerable FindCallouts(
+ IEnumerable callOuts,
+ int lineNumber
+ ) => callOuts.Where(callOut => callOut.Line == lineNumber);
+
private static int GetCommonIndent(EnhancedCodeBlock block)
{
var commonIndent = int.MaxValue;
diff --git a/src/Elastic.Markdown/Slices/Layout/_Scripts.cshtml b/src/Elastic.Markdown/Slices/Layout/_Scripts.cshtml
index 1371c4565..9cfec421e 100644
--- a/src/Elastic.Markdown/Slices/Layout/_Scripts.cshtml
+++ b/src/Elastic.Markdown/Slices/Layout/_Scripts.cshtml
@@ -16,5 +16,5 @@
-
+
diff --git a/src/Elastic.Markdown/_static/custom.css b/src/Elastic.Markdown/_static/custom.css
index f0b4e3978..80c66b610 100644
--- a/src/Elastic.Markdown/_static/custom.css
+++ b/src/Elastic.Markdown/_static/custom.css
@@ -134,4 +134,59 @@ See https://github.com/elastic/docs-builder/issues/219 for further details
--color-1: var(--gray-2);
--color-2: var(--gray-a4);
--color-3: var(--gray-10);
+}
+
+
+/* Code Callouts */
+
+.yue code span.code-callout {
+ display: inline-flex;
+ font-size: 0.75em;
+ border-radius: 99999px;
+ background-color: var(--accent-11);
+ width: 20px;
+ height: 20px;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ transform: translateY(-2px);
+}
+
+.yue code span.code-callout > span {
+ color: white;
+}
+
+.yue ol.code-callouts {
+ margin-top: 0;
+ counter-reset: code-callout-counter;
+}
+
+.yue ol.code-callouts li::before {
+ content: counter(code-callout-counter);
+ position: absolute;
+ --size: 20px;
+ left: calc(-1 * var(--size) - 5px);
+ top: 5px;
+ color: white;
+ display: inline-flex;
+ font-size: 0.75em;
+ border-radius: 99999px;
+ background-color: var(--accent-11);
+ width: var(--size);
+ height: var(--size);
+ align-items: center;
+ justify-content: center;
+ margin: 0 0.25em;
+ transform: translateY(-2px);
+ font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
+}
+
+.yue ol.code-callouts li {
+ margin: 0 0 0.5rem 0;
+ counter-increment: code-callout-counter;
+ position: relative;
+}
+
+.yue ol.code-callouts li::marker {
+ display: none;
}
\ No newline at end of file
diff --git a/src/Elastic.Markdown/_static/hljs.js b/src/Elastic.Markdown/_static/hljs.js
new file mode 100644
index 000000000..1f757b3d9
--- /dev/null
+++ b/src/Elastic.Markdown/_static/hljs.js
@@ -0,0 +1,186 @@
+(function () {
+ // The merge HTMLPlugin was copied from https://github.com/highlightjs/highlight.js/issues/2889
+ var mergeHTMLPlugin = (function () {
+ 'use strict';
+
+ var originalStream;
+
+ /**
+ * @param {string} value
+ * @returns {string}
+ */
+ function escapeHTML(value) {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ /* plugin itself */
+
+ /** @type {HLJSPlugin} */
+ const mergeHTMLPlugin = {
+ // preserve the original HTML token stream
+ "before:highlightElement": ({ el }) => {
+ originalStream = nodeStream(el);
+ },
+ // merge it afterwards with the highlighted token stream
+ "after:highlightElement": ({ el, result, text }) => {
+ if (!originalStream.length) return;
+
+ const resultNode = document.createElement('div');
+ resultNode.innerHTML = result.value;
+ result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
+ el.innerHTML = result.value;
+ }
+ };
+
+ /* Stream merging support functions */
+
+ /**
+ * @typedef Event
+ * @property {'start'|'stop'} event
+ * @property {number} offset
+ * @property {Node} node
+ */
+
+ /**
+ * @param {Node} node
+ */
+ function tag(node) {
+ return node.nodeName.toLowerCase();
+ }
+
+ /**
+ * @param {Node} node
+ */
+ function nodeStream(node) {
+ /** @type Event[] */
+ const result = [];
+ (function _nodeStream(node, offset) {
+ for (let child = node.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === 3) {
+ offset += child.nodeValue.length;
+ } else if (child.nodeType === 1) {
+ result.push({
+ event: 'start',
+ offset: offset,
+ node: child
+ });
+ offset = _nodeStream(child, offset);
+ // Prevent void elements from having an end tag that would actually
+ // double them in the output. There are more void elements in HTML
+ // but we list only those realistically expected in code display.
+ if (!tag(child).match(/br|hr|img|input/)) {
+ result.push({
+ event: 'stop',
+ offset: offset,
+ node: child
+ });
+ }
+ }
+ }
+ return offset;
+ })(node, 0);
+ return result;
+ }
+
+ /**
+ * @param {any} original - the original stream
+ * @param {any} highlighted - stream of the highlighted source
+ * @param {string} value - the original source itself
+ */
+ function mergeStreams(original, highlighted, value) {
+ let processed = 0;
+ let result = '';
+ const nodeStack = [];
+
+ function selectStream() {
+ if (!original.length || !highlighted.length) {
+ return original.length ? original : highlighted;
+ }
+ if (original[0].offset !== highlighted[0].offset) {
+ return (original[0].offset < highlighted[0].offset) ? original : highlighted;
+ }
+
+ /*
+ To avoid starting the stream just before it should stop the order is
+ ensured that original always starts first and closes last:
+
+ if (event1 == 'start' && event2 == 'start')
+ return original;
+ if (event1 == 'start' && event2 == 'stop')
+ return highlighted;
+ if (event1 == 'stop' && event2 == 'start')
+ return original;
+ if (event1 == 'stop' && event2 == 'stop')
+ return highlighted;
+
+ ... which is collapsed to:
+ */
+ return highlighted[0].event === 'start' ? original : highlighted;
+ }
+
+ /**
+ * @param {Node} node
+ */
+ function open(node) {
+ /** @param {Attr} attr */
+ function attributeString(attr) {
+ return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
+ }
+ // @ts-ignore
+ result += '<' + tag(node) + [].map.call(node.attributes, attributeString).join('') + '>';
+ }
+
+ /**
+ * @param {Node} node
+ */
+ function close(node) {
+ result += '' + tag(node) + '>';
+ }
+
+ /**
+ * @param {Event} event
+ */
+ function render(event) {
+ (event.event === 'start' ? open : close)(event.node);
+ }
+
+ while (original.length || highlighted.length) {
+ let stream = selectStream();
+ result += escapeHTML(value.substring(processed, stream[0].offset));
+ processed = stream[0].offset;
+ if (stream === original) {
+ /*
+ On any opening or closing tag of the original markup we first close
+ the entire highlighted node stack, then render the original tag along
+ with all the following original tags at the same offset and then
+ reopen all the tags on the highlighted stack.
+ */
+ nodeStack.reverse().forEach(close);
+ do {
+ render(stream.splice(0, 1)[0]);
+ stream = selectStream();
+ } while (stream === original && stream.length && stream[0].offset === processed);
+ nodeStack.reverse().forEach(open);
+ } else {
+ if (stream[0].event === 'start') {
+ nodeStack.push(stream[0].node);
+ } else {
+ nodeStack.pop();
+ }
+ render(stream.splice(0, 1)[0]);
+ }
+ }
+ return result + escapeHTML(value.substr(processed));
+ }
+
+ return mergeHTMLPlugin;
+ }());
+
+ hljs.addPlugin(mergeHTMLPlugin);
+ hljs.highlightAll();
+})();
\ No newline at end of file