Embedded Vpatch Formatting for MP-WP, Draft Vpatch for Review

January 28th, 2020

After some discussion around what should be included in a first version of this feature I have published for review a draft vpatch of the mp-wp patch viewer plugin. There are several things to call out in the code so let's go through this line-by-line:

#S1-L9: I think this file should be renamed to something like mp-wp-content-processing.php but left it as is for now to provide a meaningful diff for review (rather than the wholesale file deleted / file added you'd get otherwise).

#S1-L22: This is not so much to claim authorship but rather to claim ownership1 and to make it clear that the world starts here.

#S1-L47: These constants were moved inside the class. I couldn't see any reason for them to remain global. The WP_FOOTNOTES_VERSION was removed because its only purpose was to help manage the options state between db and file.

#S1-L63: For obvious reasons, I can't render the codeblock delimiters inside a codeblock delimited by the codeblock delimiters2—which, in this patch, I have set to [[ and /]]. I grepped through the trb genesis and found no matches, and in all 162k! lines of the mp-wp genesis3 there were only three instances of [[ and six of /]]. It appears to be safe for bash as well, at least based on what I could find in the wild. I personally like these delimiters because they are succinct and follow closely the established pattern of (( )) for footnotes.

#S1-L102: This had to come before the wpautop filter which is responsible for inserting <p> and <br /> tags all over the place. Thankfully wpautop ignores content within <pre> tags so that its ill effects can be avoided by processing the codeblocks before it has a chance to run4. The priority for footnotes processing was left unchanged.

Known limitations: Some stress testing that I did locally revealed an upper limit for a single snippet of approximately 5k lines of code. Adding multiple ~5k snippets to a single article topped out around roughly ~20k lines (4 snippets). Testing with ~525 line snippets failed somewhere between 55-60 snippets on the page (so getting really close to 30k lines there before bumping into the 128MB default memory limit for a PHP script). I have not yet tried these tests with an increased memory limit since the results seemed more than sufficient for how it would be used.

That covers the bulk of it, the rest should be self-explanatory. In terms of overall design, I did think of combining the footnotes and codeblock processing passes into a single filter but we would have to get around the issue of wpautop (codeblocks coming before and footnotes after). However, I couldn't see a performance justification for this since the extraction -> formatting -> reinsertion loops would still need to happen in separate passes.

The patch and signature for anyone who would like to take it for a spin in the own environment:

mp-wp_add-embedded-vpatch-formatting.vpatch
mp-wp_add-embedded-vpatch-formatting.vpatch.billymg.sig

1 diff -uNr a/mp-wp/manifest b/mp-wp/manifest
2 --- a/mp-wp/manifest 4ad5c0b7eda9c670f311a23da92114ab10bf30b9990b46882047aea3ab569395f643cb4aa6144ee0ba74b9821df1e69b26d299c1f0e9c11631a78c46f95913bd
3 +++ b/mp-wp/manifest e70855d060aefbd26f51cf60e39e553e7666f8f76d49adeb95999ab34401f1984e808082f7f34690ebc31fb93f5da192aa0d74b7f5eabd3e62fae3f2054720f4
4 @@ -5,3 +5,4 @@
5 569483 mp-wp_remove-tinymce-and-other-crud billymg Remove tinymce, most of the importers, the self-update feature, and the google gears and press-this plugins
6 602064 mp-wp_apply-htmlspecialchars-to-post-edit-content billymg Run post content through htmlspecialchars() before loading into the post edit UI
7 605926 mp-wp_comments_filtering diana_coman Recent comments widget should show only people's comments (no track/pingbacks); theme default changed to show trackbacks/pingbacks as last/at the bottom in an article's comments list.
8 +614805 mp-wp_add-embedded-vpatch-formatting billymg Add the ability to embed vpatches within article content. Embedded vpatch blocks will be formatted with diff syntax highlighting and anchored line numbers
9 diff -uNr a/mp-wp/wp-content/plugins/footnotes.php b/mp-wp/wp-content/plugins/footnotes.php
10 --- a/mp-wp/wp-content/plugins/footnotes.php 8e2449d4ac26ea05f080cec9d025ef8a8585221ee30da439b37ff1578d084e1c63cbe3f89e3d6868c19d0fa9f73a9af99b444251e7a854f6c87e316628d94859
11 +++ b/mp-wp/wp-content/plugins/footnotes.php 1109723713df46f7ec135f0640d3acba475974f439d722b379581dd262b5f7bf42295ca1e57600d6bb9636a2e9d35965d3841d74fc15d5bf3952cfada98f363b
12 @@ -1,49 +1,28 @@
13 <?php
14 /*
15 -Plugin Name: WP-Footnotes
16 -Plugin URI: http://www.elvery.net/drzax/more-things/wordpress-footnotes-plugin/
17 -Version: 4.2
18 -Description: Allows a user to easily add footnotes to a post.
19 -Author: Simon Elvery
20 -Author URI: http://www.elvery.net/drzax/
21 +Plugin Name: MP-WP-Content-Processing
22 +Plugin URI: http://billymg.com/category/mp-wp/
23 +Description: Allows for the custom processing of article content. Currently supports footnotes and embedded vpatch snippets.
24 +Author: billymg
25 +Author URI: http://billymg.com
26 */
27
28 -/*
29 - * This file is part of WP-Footnotes a plugin for Word Press
30 - * Copyright (C) 2007 Simon Elvery
31 - *
32 - * This program is free software; you can redistribute it and/or
33 - * modify it under the terms of the GNU General Public License
34 - * as published by the Free Software Foundation; either version 2
35 - * of the License, or (at your option) any later version.
36 - *
37 - * This program is distributed in the hope that it will be useful,
38 - * but WITHOUT ANY WARRANTY; without even the implied warranty of
39 - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40 - * GNU General Public License for more details.
41 - *
42 - * You should have received a copy of the GNU General Public License
43 - * along with this program; if not, write to the Free Software
44 - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
45 - */
46 -
47 -// Some important constants
48 -define('WP_FOOTNOTES_OPEN', " (("); //You can change this if you really have to, but I wouldn't recommend it.
49 -define('WP_FOOTNOTES_CLOSE', "))"); //Same with this one.
50 -define('WP_FOOTNOTES_VERSION', '4.2');
51 -
52 // Instantiate the class
53 -$swas_wp_footnotes = new swas_wp_footnotes();
54 +$mp_wp_content_processing = new mp_wp_content_processing();
55
56 // Encapsulate in a class
57 -class swas_wp_footnotes {
58 - var $current_options;
59 - var $default_options;
60 +class mp_wp_content_processing {
61 + const MP_WP_FOOTNOTES_OPEN = " ((";
62 + const MP_WP_FOOTNOTES_CLOSE = "))";
63 + const MP_WP_CODEBLOCK_OPEN = "[[";
64 + const MP_WP_CODEBLOCK_CLOSE = "/]]";
65 +
66 + var $options;
67
68 /**
69 * Constructor.
70 */
71 - function swas_wp_footnotes() {
72 + function mp_wp_content_processing() {
73 // Define the implemented option styles
74 $this->styles = array(
75 'decimal' => '1,2...10',
76 @@ -56,17 +35,16 @@
77 );
78
79 // Define default options
80 - $this->default_options = array('superscript'=>true,
81 + $this->options = array('superscript'=>true,
82 'pre_backlink'=>' [',
83 'backlink'=>'&#8617;',
84 'post_backlink'=>']',
85 'pre_identifier'=>'',
86 - 'list_style_type'=>'decimal',
87 + 'list_style_type'=>'lower-roman',
88 'list_style_symbol'=>'&dagger;',
89 'post_identifier'=>'',
90 'pre_footnotes'=>'',
91 'post_footnotes'=>'',
92 - 'style_rules'=>'ol.footnotes{font-size:0.8em; color:#666666;}',
93 'no_display_home'=>false,
94 'no_display_archive'=>false,
95 'no_display_date'=>false,
96 @@ -74,58 +52,80 @@
97 'no_display_search'=>false,
98 'no_display_feed'=>false,
99 'combine_identical_notes'=>false,
100 - 'priority'=>11,
101 - 'version'=>WP_FOOTNOTES_VERSION);
102 + 'codeblocks_priority'=>9, // highest value that comes before wpautop filter
103 + 'footnotes_priority'=>11);
104
105 - // Get the current settings or setup some defaults if needed
106 - if (!$this->current_options = get_option('swas_footnote_options')){
107 - $this->current_options = $this->default_options;
108 - update_option('swas_footnote_options', $this->current_options);
109 - } else {
110 - // Set any unset options
111 - if ($this->current_options['version'] != WP_FOOTNOTES_VERSION) {
112 - foreach ($this->default_options as $key => $value) {
113 - if (!isset($this->current_options[$key])) {
114 - $this->current_options[$key] = $value;
115 - }
116 - }
117 - $this->current_options['version'] = WP_FOOTNOTES_VERSION;
118 - update_option('swas_footnote_options', $this->current_options);
119 - }
120 + // Hook me up
121 + add_action('the_content', array($this, 'process_codeblocks'), $this->options['codeblocks_priority']);
122 + add_action('the_content', array($this, 'process_footnotes'), $this->options['footnotes_priority']);
123 + add_action('wp_head', array($this, 'insert_styles'));
124 + }
125 +
126 + /**
127 + * Searches the text and apply markup to codeblocks.
128 + * Adds line number links and diff syntax highlighting.
129 + * @param $data string The content of the post.
130 + * @return string The new content with formatted codeblocks.
131 + */
132 + function process_codeblocks($data) {
133 + global $post;
134 +
135 + // Regex extraction of all codeblocks (or return if there are none)
136 + if (
137 + !preg_match_all(
138 + "/(".preg_quote(self::MP_WP_CODEBLOCK_OPEN).")(.*)(".preg_quote(self::MP_WP_CODEBLOCK_CLOSE, '/').")/Us",
139 + $data,
140 + $codeblocks,
141 + PREG_SET_ORDER
142 + )
143 + ) {
144 + return $data;
145 }
146
147 -/*
148 - if (!empty($_POST['save_options'])){
149 - $footnotes_options['superscript'] = (array_key_exists('superscript', $_POST)) ? true : false;
150 - $footnotes_options['pre_backlink'] = $_POST['pre_backlink'];
151 - $footnotes_options['backlink'] = $_POST['backlink'];
152 - $footnotes_options['post_backlink'] = $_POST['post_backlink'];
153 - $footnotes_options['pre_identifier'] = $_POST['pre_identifier'];
154 - $footnotes_options['list_style_type'] = $_POST['list_style_type'];
155 - $footnotes_options['post_identifier'] = $_POST['post_identifier'];
156 - $footnotes_options['list_style_symbol'] = $_POST['list_style_symbol'];
157 - $footnotes_options['pre_footnotes'] = stripslashes($_POST['pre_footnotes']);
158 - $footnotes_options['post_footnotes'] = stripslashes($_POST['post_footnotes']);
159 - $footnotes_options['style_rules'] = stripslashes($_POST['style_rules']);
160 - $footnotes_options['no_display_home'] = (array_key_exists('no_display_home', $_POST)) ? true : false;
161 - $footnotes_options['no_display_archive'] = (array_key_exists('no_display_archive', $_POST)) ? true : false;
162 - $footnotes_options['no_display_date'] = (array_key_exists('no_display_date', $_POST)) ? true : false;
163 - $footnotes_options['no_display_category'] = (array_key_exists('no_display_category', $_POST)) ? true : false;
164 - $footnotes_options['no_display_search'] = (array_key_exists('no_display_search', $_POST)) ? true : false;
165 - $footnotes_options['no_display_feed'] = (array_key_exists('no_display_feed', $_POST)) ? true : false;
166 - $footnotes_options['combine_identical_notes'] = (array_key_exists('combine_identical_notes', $_POST)) ? true : false;
167 - $footnotes_options['priority'] = $_POST['priority'];
168 - update_option('swas_footnote_options', $footnotes_options);
169 - }elseif(!empty($_POST['reset_options'])){
170 - update_option('swas_footnote_options', '');
171 - update_option('swas_footnote_options', $this->default_options);
172 + for ($i = 0; $i < count($codeblocks); $i++) {
173 + $codeblocks[$i]['snippet'] = $this->format_snippet($codeblocks[$i][2], $i+1);
174 }
175 -*/
176
177 - // Hook me up
178 - add_action('the_content', array($this, 'process'), $this->current_options['priority']);
179 - add_action('admin_menu', array($this, 'add_options_page')); // Insert the Admin panel.
180 - add_action('wp_head', array($this, 'insert_styles'));
181 + foreach ($codeblocks as $key => $value) {
182 + $data = substr_replace($data, $value['snippet'], strpos($data,$value[0]),strlen($value[0]));
183 + }
184 +
185 + return $data;
186 + }
187 +
188 + function format_snippet($snippet, $snippet_number) {
189 + $formatted_snippet = htmlspecialchars($snippet);
190 + $code_lines = explode("\r\n", $formatted_snippet);
191 +
192 + foreach ($code_lines as $idx => $line) {
193 + $line_number = sprintf('S%d-L%d', $snippet_number, $idx+1);
194 + $line_link = sprintf('<a href="#%s" name="%s">%d</a>', $line_number, $line_number, $idx+1);
195 + $line_open = sprintf('<tr><td class="line-number-column">%s</td><td class="content-column">', $line_link);
196 + $line_close = '</td></tr>';
197 +
198 + if (substr($line, 0, 5) == 'diff ') {
199 + $code_lines[$idx] = sprintf('%s<span class="line-filename">%s</span>%s', $line_open, $line, $line_close);
200 + } elseif (substr($line, 0, 4) == '--- ' || substr($line, 0, 4) == '+++ ' || substr($line, 0, 3) == '@@ ') {
201 + $code_lines[$idx] = sprintf('%s<span class="line-meta">%s</span>%s', $line_open, $line, $line_close);
202 + } elseif (substr($line, 0, 1) == '-') {
203 + $code_lines[$idx] = sprintf('%s<span class="line-removed">%s</span>%s', $line_open, $line, $line_close);
204 + } elseif (substr($line, 0, 1) == '+') {
205 + $code_lines[$idx] = sprintf('%s<span class="line-added">%s</span>%s', $line_open, $line, $line_close);
206 + } else {
207 + $code_lines[$idx] = sprintf('%s<span class="line-default">%s</span>%s', $line_open, $line, $line_close);
208 + }
209 + }
210 +
211 + $formatted_snippet = implode("\n", $code_lines);
212 +
213 + $formatted_snippet = sprintf(
214 + '%s%s%s',
215 + '<pre class="mp-wp-codeblock"><table cellpadding="0" cellspacing="0"><tbody>',
216 + $formatted_snippet,
217 + '</tbody></table></pre>'
218 + );
219 +
220 + return $formatted_snippet;
221 }
222
223 /**
224 @@ -134,25 +134,28 @@
225 * @param $data string The content of the post.
226 * @return string The new content with footnotes generated.
227 */
228 - function process($data) {
229 + function process_footnotes($data) {
230 global $post;
231
232 // Check for and setup the starting number
233 $start_number = (preg_match("|<!\-\-startnum=(\d+)\-\->|",$data,$start_number_array)==1) ? $start_number_array[1] : 1;
234
235 - // Regex extraction of all footnotes (or return if there are none)
236 - if (!preg_match_all("/(".preg_quote(WP_FOOTNOTES_OPEN)."|<footnote>)(.*)(".preg_quote(WP_FOOTNOTES_CLOSE)."|<\/footnote>)/Us", $data, $identifiers, PREG_SET_ORDER)) {
237 + // Remove codeblocks from content to be parsed for footnotes
238 + $data_sans_codeblocks = preg_replace("/(<pre class=\"mp-wp-codeblock\">)(.*)(<\/pre>)/Us", '', $data);
239 +
240 + // Regex extraction of all footnotes from non-codeblock content (or return if there are none)
241 + if (!preg_match_all("/(".preg_quote(self::MP_WP_FOOTNOTES_OPEN)."|<footnote>)(.*)(".preg_quote(self::MP_WP_FOOTNOTES_CLOSE)."|<\/footnote>)/Us", $data_sans_codeblocks, $identifiers, PREG_SET_ORDER)) {
242 return $data;
243 }
244
245 // Check whether we are displaying them or not
246 $display = true;
247 - if ($this->current_options['no_display_home'] && is_home()) $display = false;
248 - if ($this->current_options['no_display_archive'] && is_archive()) $display = false;
249 - if ($this->current_options['no_display_date'] && is_date()) $display = false;
250 - if ($this->current_options['no_display_category'] && is_category()) $display = false;
251 - if ($this->current_options['no_display_search'] && is_search()) $display = false;
252 - if ($this->current_options['no_display_feed'] && is_feed()) $display = false;
253 + if ($this->options['no_display_home'] && is_home()) $display = false;
254 + if ($this->options['no_display_archive'] && is_archive()) $display = false;
255 + if ($this->options['no_display_date'] && is_date()) $display = false;
256 + if ($this->options['no_display_category'] && is_category()) $display = false;
257 + if ($this->options['no_display_search'] && is_search()) $display = false;
258 + if ($this->options['no_display_feed'] && is_feed()) $display = false;
259
260 $footnotes = array();
261
262 @@ -160,7 +163,7 @@
263 if ( array_key_exists(get_post_meta($post->ID, 'footnote_style', true), $this->styles) ) {
264 $style = get_post_meta($post->ID, 'footnote_style', true);
265 } else {
266 - $style = $this->current_options['list_style_type'];
267 + $style = $this->options['list_style_type'];
268 }
269
270 // Create 'em
271 @@ -175,7 +178,7 @@
272
273
274 // if we're combining identical notes check if we've already got one like this & record keys
275 - if ($this->current_options['combine_identical_notes']){
276 + if ($this->options['combine_identical_notes']){
277 for ($j=0; $j<count($footnotes); $j++){
278 if ($footnotes[$j]['text'] == $identifiers[$i]['text']){
279 $identifiers[$i]['use_footnote'] = $j;
280 @@ -206,12 +209,9 @@
281 $id_id = "identifier_".$key."_".$post->ID;
282 $id_num = ($style == 'decimal') ? $value['use_footnote']+$start_number : $this->convert_num($value['use_footnote']+$start_number, $style, count($footnotes));
283 $id_href = ( ($use_full_link) ? get_permalink($post->ID) : '' ) . "#footnote_".$value['use_footnote']."_".$post->ID;
284 -
285 -// $id_title = str_replace('"', "&quot;", htmlentities(strip_tags($value['text']), ENT_QUOTES, 'UTF-8'));
286 -
287 $id_title = str_replace('"', '`', strip_tags($value['text']));
288 - $id_replace = $this->current_options['pre_identifier'].'<a href="'.$id_href.'" id="'.$id_id.'" class="footnote-link footnote-identifier-link" title="'.$id_title.'">'.$id_num.'</a>'.$this->current_options['post_identifier'];
289 - if ($this->current_options['superscript']) $id_replace = '<sup>'.$id_replace.'</sup>';
290 + $id_replace = $this->options['pre_identifier'].'<a href="'.$id_href.'" id="'.$id_id.'" class="footnote-link footnote-identifier-link" title="'.$id_title.'">'.$id_num.'</a>'.$this->options['post_identifier'];
291 + if ($this->options['superscript']) $id_replace = '<sup>'.$id_replace.'</sup>';
292 if ($display) $data = substr_replace($data, $id_replace, strpos($data,$value[0]),strlen($value[0]));
293 else $data = substr_replace($data, '', strpos($data,$value[0]),strlen($value[0]));
294 }
295 @@ -219,14 +219,14 @@
296 // Display footnotes
297 if ($display) {
298 $start = ($start_number != 1) ? 'start="'.$start_number.'" ' : '';
299 - $data = $data.$this->current_options['pre_footnotes'];
300 + $data = $data.$this->options['pre_footnotes'];
301
302 $data = $data . '<ol '.$start.'class="footnotes">';
303 foreach ($footnotes as $key => $value) {
304 $data = $data.'<li id="footnote_'.$key.'_'.$post->ID.'" class="footnote"';
305 if ($style == 'symbol') {
306 $data = $data . ' style="list-style-type:none;"';
307 - } elseif($style != $this->current_options['list_style_type']) {
308 + } elseif($style != $this->options['list_style_type']) {
309 $data = $data . ' style="list-style-type:' . $style . ';"';
310 }
311 $data = $data . '>';
312 @@ -236,56 +236,39 @@
313 $data = $data.$value['text'];
314 if (!is_feed()){
315 foreach($value['identifiers'] as $identifier){
316 - $data = $data.$this->current_options['pre_backlink'].'<a href="'.( ($use_full_link) ? get_permalink($post->ID) : '' ).'#identifier_'.$identifier.'_'.$post->ID.'" class="footnote-link footnote-back-link">'.$this->current_options['backlink'].'</a>'.$this->current_options['post_backlink'];
317 + $data = $data.$this->options['pre_backlink'].'<a href="'.( ($use_full_link) ? get_permalink($post->ID) : '' ).'#identifier_'.$identifier.'_'.$post->ID.'" class="footnote-link footnote-back-link">'.$this->options['backlink'].'</a>'.$this->options['post_backlink'];
318 }
319 }
320 $data = $data . '</li>';
321 }
322 - $data = $data . '</ol>' . $this->current_options['post_footnotes'];
323 + $data = $data . '</ol>' . $this->options['post_footnotes'];
324 }
325 return $data;
326 }
327
328 - /**
329 - * Really insert the options page.
330 - */
331 - function footnotes_options_page() {
332 - $this->current_options = get_option('swas_footnote_options');
333 - foreach ($this->current_options as $key=>$setting) {
334 - $new_setting[$key] = htmlentities($setting);
335 - }
336 - $this->current_options = $new_setting;
337 - unset($new_setting);
338 - include (dirname(__FILE__) . '/options.php');
339 - }
340 -
341 - /**
342 - * Insert the options page into the admin area.
343 - */
344 - function add_options_page() {
345 - // Add a new menu under Options:
346 - add_options_page('Footnotes', 'Footnotes', 8, __FILE__, array($this, 'footnotes_options_page'));
347 - }
348 -
349 - function upgrade_post($data){
350 - $data = str_replace('<footnote>',WP_FOOTNOTES_OPEN,$data);
351 - $data = str_replace('</footnote>',WP_FOOTNOTES_CLOSE,$data);
352 - return $data;
353 - }
354 -
355 - function insert_styles(){
356 + function insert_styles() {
357 ?>
358 <style type="text/css">
359 - <?php if ($this->current_options['list_style_type'] != 'symbol'): ?>
360 - ol.footnotes li {list-style-type:<?php echo $this->current_options['list_style_type']; ?>;}
361 + ol.footnotes { font-size: 0.8em; color: #666666; }
362 + pre.mp-wp-codeblock { background: none; color: #333; border: 1px solid #ddd; padding: 0; }
363 + td.line-number-column { background: #f5f6f7; text-align: right; }
364 + td.line-number-column a { color: #555; padding: 0 5px; }
365 + td.content-column { padding-left: 10px; }
366 + span.line-filename { font-weight: bold; }
367 + span.line-meta { color: #999; }
368 + span.line-added { color: green; }
369 + span.line-removed { color:red; }
370 +
371 + <?php if ($this->options['list_style_type'] != 'symbol'): ?>
372 + ol.footnotes li { list-style-type: <?php echo $this->options['list_style_type']; ?>; }
373 <?php endif; ?>
374 - <?php echo $this->current_options['style_rules'];?>
375 +
376 </style>
377 <?php
378 }
379
380
381 - function convert_num ($num, $style, $total){
382 + function convert_num ($num, $style, $total) {
383 switch ($style) {
384 case 'decimal-leading-zero' :
385 $width = max(2, strlen($total));
386 @@ -301,7 +284,7 @@
387 case 'symbol' :
388 $sym = '';
389 for ($i = 0; $i<$num; $i++) {
390 - $sym .= $this->current_options['list_style_symbol'];
391 + $sym .= $this->options['list_style_symbol'];
392 }
393 return $sym;
394 }
395 @@ -318,7 +301,7 @@
396 * @param string $case Upper or lower case.
397 * @return string The roman numeral
398 */
399 - function roman($num, $case= 'upper'){
400 + function roman($num, $case= 'upper') {
401 $num = (int) $num;
402 $conversion = array('M'=>1000, 'CM'=>900, 'D'=>500, 'CD'=>400, 'C'=>100, 'XC'=>90, 'L'=>50, 'XL'=>40, 'X'=>10, 'IX'=>9, 'V'=>5, 'IV'=>4, 'I'=>1);
403 $roman = '';
404 @@ -331,7 +314,7 @@
405 return ($case == 'lower') ? strtolower($roman) : $roman;
406 }
407
408 - function alpha($num, $case='upper'){
409 + function alpha($num, $case='upper') {
410 $j = 1;
411 for ($i = 'A'; $i <= 'ZZ'; $i++){
412 if ($j == $num){
413
  1. So that if something goes wrong I'm on the line to fix it, lest my reputation suffer the negratings. []
  2. I tried escaping with &lsqb; and &rsqb; (as I did for this article) but because the code snippet itself is sent through htmlspecialchars that wouldn't work either, so I decided this to be an unwinnable battle and moved on. If anyone would like to point out what I'm missing I'd be happy to update this article. []
  3. 23k of which have since been snipped. []
  4. wpautop can also be disabled at the theme level via the addition of remove_filter( 'the_content', 'wpautop' ); in the theme's function.php file. []

MP-WP Patch Viewer and Code Shelf

January 13th, 2020

After some brief discussion in #ossasepia, which itself was prompted by a suggestion from MP on Diana's blog, I decided it would be worth compiling the thoughts into a proposal and including a few wireframes for illustration.

There are actually two proposals, the first is for the ability to embed vpatch code snippets into an mp-wp article by way of some custom markup, e.g. [[ code goes here ]]. The second proposal is for providing some level of vpatch management from within mp-wp, to facilitate the concept of a code shelf as well as a better format for full-length vpatch1 reviews. I'll discuss the former proposal first because it makes sense as a standalone whereas the second proposal requires work from the first. So from an implementation and release standpoint it makes sense to support embeddable snippets before full-length vpatch management.

Embeddable VPatch Snippets

Embedded Code Snippets Full Embedded VPatch

This feature would:

  • Allow users to embed vpatch content within an article via some new markup, e.g. [[]], similar to (()) for footnotes
  • Display patches with diff syntax highlighting2 and line numbers
  • Allow linking to anchored line numbers. Clicking a line number navigates to its anchor, e.g. clicking line '22' appends '#L22' to the URL. useful for sharing in the logs or referencing in comments on the article

This is pretty straightforward. It would only require a new mp-wp plugin to parse the new markup and minor updates to a theme to support the diff formatting. This feature would work great for most quick vpatch reviews, and could even be used for larger vpatch reviews for the time being.

The only thing I could see making this feature more involved would be the notion of inline comments (wireframe) in an embedded vpatch. If we wanted to do that we might have to add a new comment type (to render it separately from the other post comments) and field (to indicate the line a comment is referencing). I do think this would be useful for full-length vpatch reviews but it's also something that can be added in a phase two of this feature.

Code Shelf, or MP-WP as a VPatch Repository

This feature is less clear in terms of scope and implementation. It is being proposed here to solicit feedback to help determine its viability and usefulness, as well as to shape the requirements. The name and inspiration comes from Diana Coman's excellent overview/map of the current V landscape.

The basic idea is to add a set of capabilities to mp-wp to make it work seamlessly as a vpatch repository. This might roughly include:

  • The ability to upload vpatches and signatures within the mp-wp admin UI
  • Automated storage and organization of uploaded vpatches, plus the ability to manage these
  • Portability of uploaded vpatches (ability to embed in an article or include in one's Code Shelf)
  • From within the Code Shelf: links back to the articles that introduced the patches, links to download VTree tarballs

Here's an example of what a Code Shelf might look like:

Code Shelf

Having this feature could be very cool, and standardizing this could allow for aggregation of vpatches into a central (or federated) repository tool that pulls from a specified list of code shelves—something like http://btcbase.org/patches but auto-generated, and with links back to introductory blog posts.

Again, this second feature is still a little fuzzy in my head, and I haven't thought too much on how it would be implemented, so I'm sharing this mainly to get others thinking about it and collect feedback from anyone who is interested. There are also still a number of other items in the mp-wp queue3 so it might be worth getting an updated priority ranking as well.

  1. This is only intuition for now, but I currently think it would be easier to deal with full vpatches if they were their own entity, rather than text inside an article. For example, they could be stored in a new mysql table, which would allow for querying and referencing without having to parse article text.

    It also seems like it would be a more pleasant experience when editing an article to update a reference to a vpatch rather than finding the opening '[[' and then scrolling however far down until you find the ']]' and making sure you paste exactly between them (for full patches of decent size, if that's what you wanted to include). []
  2. In theory this could be expanded later to include support for other syntaxes besides UNIX diff files. []
  3. Completion of the test suite, slimming patches, a proper theme or two, the addition of the new server-side select mechanism as a standard feature (and removal of the old mechanism), and bulk upload support. []

Travelog: Costa Rica

December 2nd, 2019

I made the trip to Costa Rica again about two six weeks ago to close on a property I first visited in August. The property is a small ranch type plot of land about 30 minutes in from the northern pacific coast of Costa Rica. This appealed to me because it would keep me well secluded from the roving tourists but nearby enough to eat in the restaurants that cater to them (and I guess to enjoy the beach now and then as well).

Getting there from central Texas is a full day affair, two if the timing of flights has you staying in San Jose for a night (the domestic flights, which are done in small single engine prop planes, don't fly at night). The last time I visited I arrived in San Jose in the afternoon and hopped on the local flight to Liberia directly after arriving, bringing me to my final destination by early evening.

img_6858

On this recent trip, however, my international flight arrived after sunset and so there were no local flights available until the next morning. This was fine since I was staying for a full week, and in fact made the travel experience more relaxed overall. I stayed in the same nice little hotel that I stayed in a year ago when passing through to visit the Lake Arenal region during my first trip to Costa Rica. The hotel is certainly nothing to write home about but for only $80/night it's perfectly adequate and even a bit charming. There's also a decent local restauarant attached where I had a nice filet of salmon over salad for dinner and a delicious slice of pecan pie for dessert. Add an appetizer and a few drinks and your total bill will be about what the room next door cost you. Not bad at all.

When I got to my room after dinner and started to unpack a few things from my backpack I realized I had neglected to pack my cellphone charger. I searched for a nearby stores that might carry the type of charger I needed so that I could pick one up in the morning before my flight. This led me the next morning to...

That's right, iCon! You can't be upset if you walk away feeling scammed, it's right there in the name after all. To be fair though, they had what I needed and the local markup (due to import tax most likely) was not outrageous. The only part of the experience that bothered me was when the cashier asked me to present "ID" when I handed him my credit card. I declined and handed him a crisp benjie instead. They had just opened so he went to the back of the store to see if he could make change. When he returned with my original note, unable to break it, I handed him the same card once again. This time he did not need to see my ID. Weird1.

Anyway, charger in hand, it was time to board another little prop plane and head to Liberia again.


When I visited in August the realtor picked me up from the airport and was my sole mode of transportion during my three day visit. This time I rented a car and drove myself. Seeing the property again two months later I was still just as happy with it as the first time I saw it.








Yes, the property even comes with cows. They belong to the groundskeeper who at first seemed quite distressed at the possibility I might not allow him to continue his little operation. I assured him that not only would it be fine for him to keep them but that I was also interested in perhaps joining his venture by providing the funds for grain finishing (in order to fatten them up enough for auction where they'd fetch a higher price than he's currently getting). It might not be a bad hobby, and at the very least my own steak needs would be covered.

I also spent some time seeing a bit of Tamarindo and other parts of the coastline. It seemed like a typical tourist surf town. There were lots of small cafes and restaurants, gift shops, little hotels, etc. The beaches were nice and not overcrowded, although I hear this changes in December and early January during the high season.


Some gringo tourists enjoying the beach on a cloudy day



A more upscale grocery store in Tamarindo

The view from another property overlooking Potrero Bay

That's pretty much all I have to report for now. The fiat job has been unusually busy these past few weeks so most of my energy is directed there. I'm also mentally preparing myself for the move and planning logistics, timeline, etc. At the moment I have only a vague sense of how it will all play out but the picture is becoming clearer by the day.

  1. The hotel clerk tried to pull this same thing on me, asking me to write down my passport number on the checkin form. I declined saying that I was never asked to do so in the past and couldn't understand why they would need it. I think she realized that I knew it was a made up "rule" and quickly backtracked, not mentioning it again. Where they get these ridiculous notions I have no idea []