2019-10-12 21:45:49 +00:00
|
|
|
/*
|
2019-12-29 22:31:32 +00:00
|
|
|
* Adapted Highlight.js Internal APIs
|
|
|
|
|
* Used to highlight selected html elements using context
|
2019-10-12 21:45:49 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/* Utility functions */
|
|
|
|
|
|
|
|
|
|
function escape(value: string): string {
|
|
|
|
|
return value
|
2019-12-29 22:31:32 +00:00
|
|
|
.replace(/&/gm, '&')
|
|
|
|
|
.replace(/</gm, '<')
|
|
|
|
|
.replace(/>/gm, '>');
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tag(node: Node): string {
|
|
|
|
|
return node.nodeName.toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Stream merging */
|
|
|
|
|
|
|
|
|
|
type NodeEvent = {
|
2019-12-29 22:31:32 +00:00
|
|
|
event: 'start' | 'stop';
|
2019-10-12 21:45:49 +00:00
|
|
|
offset: number;
|
|
|
|
|
node: Node;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function nodeStream(node: Node): NodeEvent[] {
|
|
|
|
|
const result: NodeEvent[] = [];
|
|
|
|
|
|
|
|
|
|
const nodeStream = (node: Node, offset: number): number => {
|
|
|
|
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
|
|
|
if (child.nodeType === 3 && child.nodeValue !== null) {
|
|
|
|
|
offset += child.nodeValue.length;
|
|
|
|
|
} else if (child.nodeType === 1) {
|
|
|
|
|
result.push({
|
2019-12-29 22:31:32 +00:00
|
|
|
event: 'start',
|
2019-10-12 21:45:49 +00:00
|
|
|
offset: offset,
|
2019-12-29 22:31:32 +00:00
|
|
|
node: child,
|
2019-10-12 21:45:49 +00:00
|
|
|
});
|
|
|
|
|
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({
|
2019-12-29 22:31:32 +00:00
|
|
|
event: 'stop',
|
2019-10-12 21:45:49 +00:00
|
|
|
offset: offset,
|
2019-12-29 22:31:32 +00:00
|
|
|
node: child,
|
2019-10-12 21:45:49 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return offset;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nodeStream(node, 0);
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mergeStreams(original: NodeEvent[], highlighted: NodeEvent[], value: string): string {
|
|
|
|
|
let processed = 0;
|
2019-12-29 22:31:32 +00:00
|
|
|
let result = '';
|
2019-10-12 21:45:49 +00:00
|
|
|
const nodeStack = [];
|
|
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
function isElement(arg?: unknown): arg is Element {
|
|
|
|
|
return arg !== null && (arg as Element)?.attributes !== undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-12 21:45:49 +00:00
|
|
|
function selectStream(): NodeEvent[] {
|
|
|
|
|
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:
|
|
|
|
|
*/
|
2019-12-29 22:31:32 +00:00
|
|
|
return highlighted[0].event === 'start' ? original : highlighted;
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function open(node: Node): void {
|
2020-01-25 23:49:53 +00:00
|
|
|
if (!isElement(node)) {
|
|
|
|
|
throw new Error('Node is not an Element');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result += `<${tag(node)} ${Array<Attr>()
|
|
|
|
|
.map.call(node.attributes, attr => `${attr.nodeName}="${escape(attr.value)}"`)
|
2019-12-29 22:31:32 +00:00
|
|
|
.join(' ')}>`;
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function close(node: Node): void {
|
2019-12-29 22:31:32 +00:00
|
|
|
result += '</' + tag(node) + '>';
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render(event: NodeEvent): void {
|
2019-12-29 22:31:32 +00:00
|
|
|
(event.event === 'start' ? open : close)(event.node);
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (original.length || highlighted.length) {
|
|
|
|
|
let stream = selectStream();
|
|
|
|
|
result += escape(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 {
|
2019-12-29 22:31:32 +00:00
|
|
|
if (stream[0].event === 'start') {
|
2019-10-12 21:45:49 +00:00
|
|
|
nodeStack.push(stream[0].node);
|
|
|
|
|
} else {
|
|
|
|
|
nodeStack.pop();
|
|
|
|
|
}
|
|
|
|
|
render(stream.splice(0, 1)[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result + escape(value.substr(processed));
|
|
|
|
|
}
|