Fix various bugs; add simplehtmldom
[pmwiki-gist-embed.git] / gist-embed.php
blobf5577fbb745a11833458d70eae9a8ab0e53f1843
1 <?php if (!defined('PmWiki')) exit();
2 /** \pastebin-embed.php
3 * \Copyright 2017-2021 Said Achmiz
4 * \Licensed under the MIT License
5 * \brief Embed Gists in a wikipage.
6 */
8 $RecipeInfo['GistEmbed']['Version'] = '2021-12-11';
10 ## (:gist-embed:)
11 Markup('gist-embed', '<fulltext', '/\(:gist-embed\s+(.+?)\s*:\)/', 'GistEmbed');
13 SDV($GistEmbedHighlightStyle, "background-color: yellow;");
15 function GistEmbed($m) {
16 static $id = 1;
18 ## Parse arguments to the markup.
19 $parsed = ParseArgs($m[1]);
21 ## These are the “bare” arguments (ones which don’t require a key, just value(s)).
22 $args = $parsed[''];
23 $gist_id = $args[0];
24 $noJS = in_array('no-js', $args);
25 $noFooter = in_array('nofooter', $args);
26 $noLineNumbers = in_array('nolinenums', $args);
27 $raw = in_array('raw', $args);
28 $noPre = in_array('no-pre', $args);
30 ## Check whether specific files are being specified.
31 $files = array();
32 if ($args[1] && !in_array($args[1], array('no-js', 'nofooter', 'nolinenums', 'raw', 'no-pre')))
33 $files = explode(',',$args[1]);
35 ## Convert the comma-delimited line ranges to an array containing each line to be
36 ## included as values.
37 ## Note that the line numbers will be zero-indexed (for use with raw text, etc.).
38 $line_ranges = $parsed['lines'] ? explode(',', $parsed['lines']) : array();
39 $line_numbers = array();
40 $to_end_from = -1;
41 foreach ($line_ranges as $key => $line_range) {
42 if (preg_match("/([0-9]+)[-–]([0-9]+)/", $line_range, $m)) {
43 $line_numbers = array_merge($line_numbers, range(--$m[1],--$m[2]));
44 } else if (preg_match("/([0-9]+)[-–]$/", $line_range, $m)) {
45 $line_numbers[] = $to_end_from = --$m[1];
46 } else {
47 $line_numbers[] = --$line_range;
51 ## Same thing, but for highlighted line ranges.
52 $hl_line_ranges = $parsed['hl'] ? explode(',', $parsed['hl']) : array();
53 $hl_line_numbers = array();
54 $hl_to_end_from = -1;
55 foreach ($hl_line_ranges as $key => $hl_line_range) {
56 if (preg_match("/([0-9]+)[-–]([0-9]+)/", $hl_line_range, $m)) {
57 $hl_line_numbers = array_merge($hl_line_numbers, range(--$m[1],--$m[2]));
58 } else if (preg_match("/([0-9]+)[-–]$/", $hl_line_range, $m)) {
59 $hl_line_numbers[] = $hl_to_end_from = --$m[1];
60 } else {
61 $hl_line_numbers[] = --$hl_line_range;
65 $embed_js_url = "https://gist.github.com/$gist_id.js";
66 $embed_raw_url = "https://gist.github.com/$gist_id/raw/";
67 $embed_json_url = "https://gist.github.com/$gist_id.json";
69 $out = "<span class='gist-embed-error'>Unknown error.</span>";
71 if ($raw) {
72 ## If no filenames have been specified, we'll have to retrieve the file list from
73 ## the server; otherwise, we'll have no idea what files to request, and just
74 ## retrieving the ‘raw’ URL for a multi-file gist (with no file specified) gets
75 ## the first file only...
76 if (empty($files)) {
77 $full_gist_data = json_decode(file_get_contents($embed_json_url),true);
78 $files = $full_gist_data['files'];
81 ## The raw text of each file of a multi-file gist must be retrieved individually.
82 $out = array();
83 foreach ($files as $filename) {
84 $raw_text = file_get_contents($embed_raw_url.$filename);
85 if (!$raw_text) return Keep("<span class='gist-embed-error'>Could not retrieve gist!</span>");
87 $raw_lines = explode("\n", $raw_text);
88 ## Convert HTML entities.
89 if (!$noPre) {
90 foreach ($raw_lines as $line)
91 $line = PVSE($line);
93 ## Highlighting only works if no-pre is NOT enabled AND if we’re displaying a
94 ## single file only.
95 if ( !empty($hl_line_numbers)
96 && !$noPre
97 && count($files) == 1) {
98 if ($hl_to_end_from >= 0)
99 $hl_line_numbers = array_merge($hl_line_numbers, range($hl_to_end_from, count($raw_lines) - 1));
100 foreach ($hl_line_numbers as $l) {
101 $raw_lines[$l] = "<span class='gist-embed-highlighted-line'>" . rtrim($raw_lines[$l]) . "</span>";
104 ## Specifying line numbers only works if we’re displaying a single file only.
105 if ( !empty($line_numbers)
106 && count($files) == 1) {
107 if ($to_end_from >= 0)
108 $line_numbers = array_merge($line_numbers, range($to_end_from, count($raw_lines) - 1));
109 $raw_lines = array_intersect_key($raw_lines, array_flip($line_numbers));
111 $raw_text = implode("\n", $raw_lines);
113 ## The ‘no-pre’ option means we shouldn’t wrap the text in a <pre> tag.
114 $out[] = $noPre ? $raw_text : Keep("<pre class='escaped gistRaw' id='gistEmbed_$id_$filename'>\n" . $raw_text . "\n</pre>\n");
116 $out = implode($noPre ? "\n\n" : "", $out);
117 } else if ($noJS) {
118 include_once('simplehtmldom/simple_html_dom.php');
120 $json_content = json_decode(file_get_contents($embed_json_url),true);
122 ## The style sheet.
123 global $HTMLHeaderFmt;
124 $HTMLHeaderFmt[] = "<link rel='stylesheet' type='text/css' href='" . $json_content['stylesheet'] . "' />\n";
126 ## The HTML.
127 $content_html = str_get_html(stripcslashes($json_content['div']));
128 $content = $content_html->find("div.gist", 0);
129 $content->id = "gistEmbed_$id";
131 ## If specific files are specified, we simply delete the div.gist-file containers
132 ## that contain files we don’t want.
133 if (!empty($files)) {
134 $file_ids = preg_replace("/\./", "-", $files);
135 $gist_file_blocks = $content_html->find("div.gist-file");
136 foreach ($gist_file_blocks as $gist_file_block) {
137 if (!in_array(substr($gist_file_block->find("div.file", 0)->id, 5), $file_ids))
138 $gist_file_block->outertext = '';
142 ## Specifying line numbers only works if we’re displaying a single file only.
143 $displayed_gist_files = array_filter($content_html->find("div.gist-file"), function ($d) { return $d->outertext; });
144 if ( !empty($line_numbers)
145 && count($displayed_gist_files) == 1) {
146 $lines = reset($displayed_gist_files)->find(".js-file-line-container tr");
147 if ($to_end_from >= 0)
148 $line_numbers = array_merge($line_numbers, range($to_end_from, count($lines) - 1));
149 foreach ($lines as $l) {
150 $line_num =$l->childNodes(0)->getAttribute('data-line-number');
151 if (!in_array(--$line_num, $line_numbers))
152 $l->outertext = '';
156 ## Highlighting specific line numbers only works if we’re displaying
157 ## a single file only.
158 if ( !empty($hl_line_numbers)
159 && count($displayed_gist_files) == 1) {
160 $lines = reset($displayed_gist_files)->find(".js-file-line-container tr");
161 if ($hl_to_end_from >= 0)
162 $hl_line_numbers = array_merge($hl_line_numbers, range($hl_to_end_from, count($lines) - 1));
163 foreach ($lines as $i => $line) {
164 if (in_array($i, $hl_line_numbers)) {
165 $line->children(1)->class .= " gist-embed-highlighted-line";
170 $out = Keep($content);
171 } else {
172 $out = Keep("<script id='gistEmbedScript_$id' src='$embed_js_url'></script>");
173 $out .= Keep("
174 <script>
175 document.querySelector('#gistEmbedScript_$id').parentElement.nextSibling.id = 'gistEmbed_$id';
176 </script>
179 ## If specific files are specified, we’ll delete the div.gist-file containers
180 ## that contain files we don’t want (this script will run right after the script
181 ## that adds the content in the first place).
182 if (!empty($files)) {
183 $files_js = preg_replace("/\./", "-", "[ '" . implode("', '", $files) . "' ]");
184 $out .= Keep("
185 <script>{
186 let files = $files_js;
187 document.querySelector('#gistEmbed_$id').querySelectorAll('div.gist-file').forEach(function (gist_file_block) {
188 if (files.indexOf(gist_file_block.querySelector('div.file').id.substring(5)) == -1)
189 gist_file_block.parentElement.removeChild(gist_file_block);
190 });
191 }</script>
195 ## Specifying line numbers only works if we’re displaying a single file only.
196 if ( !empty($line_numbers)
197 || !empty($hl_line_numbers)) {
198 $line_numbers_js = "[ " . implode(", " , $line_numbers) . " ]";
199 $hl_line_numbers_js = "[ " . implode(", " , $hl_line_numbers) . " ]";
200 $out .= Keep("
201 <script>{
202 if (document.querySelector('#gistEmbed_$id').querySelectorAll('div.gist-file').length == 1) {
203 let num_lines = document.querySelector('#gistEmbed_$id').querySelector('div.gist-file').querySelectorAll('.js-file-line-container tr').length;
205 let line_numbers = $line_numbers_js;
206 let to_end_from = $to_end_from;
207 if (to_end_from >= 0)
208 line_numbers = [...line_numbers, ...[...Array(num_lines - to_end_from)].map((_, i) => to_end_from + i)];
210 let hl_line_numbers = $hl_line_numbers_js;
211 let hl_to_end_from = $hl_to_end_from;
212 if (hl_to_end_from >= 0)
213 hl_line_numbers = [...hl_line_numbers, ...[...Array(num_lines - hl_to_end_from)].map((_, i) => hl_to_end_from + i)];
215 document.querySelector('#gistEmbed_$id').querySelector('div.gist-file').querySelectorAll('.js-file-line-container tr').forEach(function (line, i) {
216 // Highlight specified line ranges (if any have been specified via the hl= parameter).
217 if (hl_line_numbers.indexOf(i) != -1)
218 line.children[1].className += ' gist-embed-highlighted-line';
220 // Filter specified line ranges (if any have been specified via the lines= parameter).
221 if (line_numbers.length > 0 && line_numbers.indexOf(i) == -1)
222 line.parentElement.removeChild(line);
225 }</script>
230 global $HTMLStylesFmt;
231 if (!$raw && $noFooter) {
232 $HTMLStylesFmt['gist-embed'][] = "#gistEmbed_$id .gist-meta { display: none; }\n";
233 $HTMLStylesFmt['gist-embed'][] = "#gistEmbed_$id .gist-data { border-bottom: none; border-radius: 2px; }\n";
235 if (!$raw && $noLineNumbers) {
236 $HTMLStylesFmt['gist-embed'][] = "#gistEmbed_$id td.js-line-number { display: none; }\n";
239 GistEmbedInjectStyles();
241 $id++;
242 return $out;
245 function GistEmbedInjectStyles() {
246 static $ran_once = false;
247 if (!$ran_once) {
248 global $HTMLStylesFmt, $GistEmbedHighlightStyle;
249 $styles = "
250 .gistRaw .gist-embed-highlighted-line { $GistEmbedHighlightStyle display: inline-block; width: calc(100% + 4px); padding-left: 4px; margin-left: -4px; }
251 .gist tr .gist-embed-highlighted-line { $GistEmbedHighlightStyle }
253 $HTMLStylesFmt['gist-embed'][] = $styles;
255 $ran_once = true;