<?php if (!defined('PmWiki')) exit();
/** \pastebin-embed.php
  * \Copyright Said Achmiz
  * \Licensed under the MIT License
  * \brief Embed Pastebin pastes in a wikipage.
  */
$RecipeInfo['PastebinEmbed']['Version'] = '2025-01-18';

## (:pastebin-embed:)
Markup('pastebin-embed', '<fulltext', '/\(:pastebin-embed\s+(.+?)\s*:\)/', 'PastebinEmbed');

SDV($PastebinEmbedHighlightStyle, "background-color: yellow;");

function PastebinEmbed ($m) {
	static $id = 1;
	
	## Parse arguments to the markup.
	$parsed = ParseArgs($m[1]);

	## These are the ‘bare’ arguments (ones which don't require a key, just value(s)).
	$args = $parsed[''];
	$paste_id = $args[0];
	$noJS = in_array('no-js', $args);
	$noFooter = in_array('nofooter', $args);
	$noLineNumbers = in_array('nolinenums', $args);
	$raw = in_array('raw', $args);
	$noPre = in_array('no-pre', $args);

	## Dark theme, if specified.
	$theme = $parsed['theme'] ?? '';
	
	## Convert the comma-delimited line ranges to an array containing each line 
	## to be included as values.
	## Note that the line numbers will be zero-indexed (for use with raw text, etc.).
	$line_ranges = isset($parsed['lines']) 
				   ? explode(',', $parsed['lines']) 
				   : [ ];
	$line_numbers = [ ];
	$to_end_from = -1;
	foreach ($line_ranges as $key => $line_range) {
		if (preg_match("/([0-9]+)[-–]([0-9]+)/", $line_range, $m)) {
			$line_numbers = array_merge($line_numbers, range(--$m[1],--$m[2]));
		} else if (preg_match("/([0-9]+)[-–]$/", $line_range, $m)) {
			$line_numbers[] = $to_end_from = --$m[1];
		} else {
			$line_numbers[] = --$line_range;
		}
	}
	
	## Same thing, but for highlighted line ranges.
	$hl_line_ranges = isset($parsed['hl']) 
					  ? explode(',', $parsed['hl']) 
					  : [ ];
	$hl_line_numbers = [ ];
	$hl_to_end_from = -1;
	foreach ($hl_line_ranges as $key => $hl_line_range) {
		if (preg_match("/([0-9]+)[-–]([0-9]+)/", $hl_line_range, $m)) {
			$hl_line_numbers = array_merge($hl_line_numbers, range(--$m[1],--$m[2]));
		} else if (preg_match("/([0-9]+)[-–]$/", $hl_line_range, $m)) {
			$hl_line_numbers[] = $hl_to_end_from = --$m[1];
		} else {
			$hl_line_numbers[] = --$hl_line_range;
		}
	}
	
	$embed_js_url = "https://pastebin.com/embed_js/$paste_id" . ($theme ? "?theme={$theme}" : "");
	$embed_iframe_url = "https://pastebin.com/embed_iframe/$paste_id" . ($theme ? "?theme={$theme}" : "");
	$embed_raw_url = "https://pastebin.com/raw/$paste_id";
	
	$out = '<span class="pastebin-embed-error">Unknown error.</span>';
	
	## There are three ‘modes’: raw (retrieve just the text, server-side, and insert it
	## into the page), no-js (retrieve the HTML, server-side, and insert it into the 
	## page), and default (i.e., JS-based: insert the scriptlet, and let it retrieve and 
	## insert the HTML into the page, client-side & async).
	## The mode is set by arguments to the (:pastebin-embed:) markup:
	## - if neither the ‘raw’ nor the ‘no-js’ option is given, default (JS) mode is used
	## - if the ‘raw’ option is given, raw mode is used
	## - if the ‘no-js’ option is given, no-js mode is used
	if ($raw) {
		$raw_text = file_get_contents($embed_raw_url);
		if (!$raw_text)
			return Keep('<span class="pastebin-embed-error">Could not retrieve paste!</span>');
		
		$raw_lines = explode("\n", $raw_text);
		## Convert HTML entities.
		if (!$noPre) {
			foreach ($raw_lines as $line)
				$line = PVSE($line);
		}
		## Highlighting only works if no-pre is NOT enabled.
		if (   !empty($hl_line_numbers) 
			&& !$noPre) {
			if ($hl_to_end_from >= 0)
				$hl_line_numbers = array_merge($hl_line_numbers, range($hl_to_end_from, count($raw_lines) - 1));
			foreach ($hl_line_numbers as $l) {
				$raw_lines[$l] = '<span class="pastebin-embed-highlighted-line">' . rtrim($raw_lines[$l]) . '</span>';
			}
		}
		## Filter by line number ranges, if specified.
		if (!empty($line_numbers)) {			
			if ($to_end_from >= 0)
				$line_numbers = array_merge($line_numbers, range($to_end_from, count($raw_lines) - 1));
			$raw_lines = array_intersect_key($raw_lines, array_flip($line_numbers));
		}
		$raw_text = implode("\n", $raw_lines);
		
		## The ‘no-pre’ option means we shouldn’t wrap the text in a <pre> tag.
		$out = $noPre 
			   ? $raw_text 
			   : Keep("<pre class='escaped embedPastebinRaw' id='pastebinEmbed_$id'>\n" . $raw_text . "\n</pre>\n");
	} else if ($noJS) {
		include_once('simple_html_dom.php');
		
		$content_html = file_get_html($embed_iframe_url);
		if (!$content_html)
			return Keep('<span class="pastebin-embed-error">Could not retrieve paste!</span>');
		$content = $content_html->find(".embedPastebin", 0);
		$content->id = "pastebinEmbed_$id";
		
		$styles_html = file_get_html($embed_js_url);
		if (!$styles_html)
			return Keep('<span class="pastebin-embed-error">Could not retrieve styles!</span>');
		$styles = $styles_html->find("style", 0);
		
		## Filter specified line ranges (if any have been specified via the lines= 
		## parameter).
		if (!empty($line_numbers)) {
			$lines = $content_html->find(".embedPastebin > ol > li");
			if ($to_end_from >= 0)
				$line_numbers = array_merge($line_numbers, range($to_end_from, count($lines) - 1));
			foreach ($lines as $i => $line) {
				if (!in_array($i, $line_numbers))
					$line->outertext = '';
				else
					$line->value = ++$i;
			}
		}
		
		## Highlight specified line ranges (if any have been specified via the hl= 
		## parameter).
		if (!empty($hl_line_numbers)) {
			$lines = $content_html->find(".embedPastebin > ol > li");
			if ($hl_to_end_from >= 0)
				$hl_line_numbers = array_merge($hl_line_numbers, range($hl_to_end_from, count($lines) - 1));
			foreach ($lines as $i => $line) {
				if (in_array($i, $hl_line_numbers)) {
					$line->children(0)->class .= " pastebin-embed-highlighted-line";
				}
			}
		}
		
		$out  = Keep($styles.$content);
	} else {
		$out  = Keep("<script id='pastebinEmbedScript_$id' src='$embed_js_url'></script>");
		$out .= Keep("
<script>
	document.querySelector('#pastebinEmbedScript_$id').parentElement.nextSibling.querySelector('.embedPastebin').id = 'pastebinEmbed_$id';
</script>
		");

		if (   !empty($hl_line_numbers) 
			|| !empty($line_numbers)) {
			$line_numbers_js = "[ " . implode(", ", $line_numbers) . " ]";
			$hl_line_numbers_js = "[ " . implode(", ", $hl_line_numbers) . " ]";
			$out .= Keep("
<script>{
	let num_lines = document.querySelector('#pastebinEmbed_$id').querySelectorAll('.embedPastebin > ol > li').length;
	
	let line_numbers = $line_numbers_js;	
	let to_end_from = $to_end_from;
	if (to_end_from >= 0)
		line_numbers = [...line_numbers, ...[...Array(num_lines - to_end_from)].map((_, i) => to_end_from + i)];

	let hl_line_numbers = $hl_line_numbers_js;
	let hl_to_end_from = $hl_to_end_from;
	if (hl_to_end_from >= 0)
		hl_line_numbers = [...hl_line_numbers, ...[...Array(num_lines - hl_to_end_from)].map((_, i) => hl_to_end_from + i)];

	document.querySelector('#pastebinEmbed_$id').querySelectorAll('.embedPastebin .source > ol > li').forEach(function (line, i) {
		// Highlight specified line ranges (if any have been specified via the hl= parameter).
		if (hl_line_numbers.indexOf(i) != -1)
			line.firstChild.className += ' pastebin-embed-highlighted-line';

		// Filter specified line ranges (if any have been specified via the lines= parameter).
		if (line_numbers.length > 0) {
			if (line_numbers.indexOf(i) == -1)
				line.parentElement.removeChild(line);
			else
				line.value = ++i;
		}
	});
}</script>
			");
		}
	}
	
	global $HTMLStylesFmt;
	if (!$raw && $noFooter) {
		$HTMLStylesFmt['pastebin-embed'][] = "#pastebinEmbed_$id .embedFooter { display: none; }\n";
	}
	if (!$raw && $noLineNumbers) {
		$HTMLStylesFmt['pastebin-embed'][] = "#pastebinEmbed_$id .source > ol { padding-left: 5px; }\n";
	}
	
	PastebinEmbedInjectStyles();
	
	$id++;
	return $out;
}

function PastebinEmbedInjectStyles() {
	static $ran_once = false;
	if (!$ran_once) {
		global $HTMLStylesFmt, $PastebinEmbedHighlightStyle;
		$styles = "
.embedPastebinRaw .pastebin-embed-highlighted-line { $PastebinEmbedHighlightStyle display: inline-block; width: calc(100% + 4px); padding-left: 4px; margin-left: -4px; }
.embedPastebin li .pastebin-embed-highlighted-line { $PastebinEmbedHighlightStyle }
#wikitext .embedPastebin ol { margin: 0; padding: 0 0 0 60px; }
";
		$HTMLStylesFmt['pastebin-embed'][] = $styles;
	}
	$ran_once = true;
}