diff2html/src/ui/js/highlight.js-helpers.ts

140 lines
4 KiB
TypeScript
Raw Normal View History

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, '&lt;')
.replace(/>/gm, '&gt;');
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 = [];
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 {
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));
}