Skip to content

Render callouts in code block #346

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

Merged
merged 4 commits into from
Jan 27, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -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($"<span class=\"code-callout\">{callOut.Index}</span>");
}

private static IEnumerable<CallOut> FindCallouts(
IEnumerable<CallOut> callOuts,
int lineNumber
) => callOuts.Where(callOut => callOut.Line == lineNumber);

private static int GetCommonIndent(EnhancedCodeBlock block)
{
var commonIndent = int.MaxValue;
Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Slices/Layout/_Scripts.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
<script>hljs.highlightAll();</script>
<script src="@Model.Static("hljs.js")"></script>
<script src="https://unpkg.com/[email protected]/dist/mermaid.min.js"></script>
55 changes: 55 additions & 0 deletions src/Elastic.Markdown/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
186 changes: 186 additions & 0 deletions src/Elastic.Markdown/_static/hljs.js
Original file line number Diff line number Diff line change
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}

/* 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();
})();
Loading