Mecha CMS

Mecha CMS blog and documentation.

Smart Internal Link for Markdown

Updated: Sunday, 07 August 2016

Automatic and maintainable internal links for Markdown.

If you are exclusively writing articles using Markdown, I would recommend you to use the simplified version of this plugin. This snippet works only for Markdown and can be used to convert any referenced link syntax in your post content into an inline link syntax with automatic internal post URL and title. You can keep using the original smart internal link plugin for cross–syntax solution.

The Converter

function do_link($id, $text = "", $o = "") {
    // Skip if we have no colon in reference ID
    if(strpos($id, ':') === false) return $o;
    $x = '<s class="text-error">' . htmlspecialchars_decode($text ? $text : 'broken') . '</s>';
    if(preg_match('#^(\w+):([a-z0-9\-]+?)(|[\#?&].*)$#', $id, $s)) {
        // Skip if method does not exist in `Get` class
        if( ! Get::kin($s[1] . 'Anchor', false, true)) return $o;
        if($post = call_user_func('Get::' . $s[1] . 'Anchor', $s[2])) {
            $text = $text ? htmlspecialchars_decode($text) : $post->title;
            $q = str_replace('&', '&amp;', $s[3]);
            $t = Text::parse($post->title, '->text') . ($q ? ' &ndash; ' . $q : "");
            return '<a href="' . $post->url . $q . '" title="' . $t . '">' . $text . '</a>';
        }
        return $x;
    }
    return $o;
}

function do_link_auto($text) {
    // Create dynamic link(s) ...
    $p = '#\[([^][]*?)\](?:\[([^][]+?)\])?#';
    return preg_replace_callback($p, function($m) {
        // handle `[post:foo-bar]` syntax
        if(strpos($m[0], '][') === false) {
            return do_link($m[1], "", $m[0]);
        // handle `[][post:foo-bar]` syntax
        } else if(strpos($m[0], '[]') === 0) {
            return do_link($m[2], "", $m[0]);
        // handle `[text][post:foo-bar]` syntax
        } else {
            return do_link($m[2], $m[1], $m[0]);
        }
    }, $text);
}

function do_link_auto_filter($text, $h) {
    // Skip if text is empty
    if( ! trim($text)) return $text;
    // Skip if we have no link(s)
    if(strpos($text, '[') === false) return $text;
    $h = (object) $h;
    // Skip if we are not using Markdown
    if(isset($h->content_type) && $h->content_type !== 'Markdown') return $text;
    // Skip text in `<(?:a|code|pre|script|style|textarea)>` element(s)
    $split = '#(<a(?:>|\s[^<>]*?>)[\s\S]*?<\/a>|<pre(?:>|\s[^<>]*?>)[\s\S]*?<\/pre>|<code(?:>|\s[^<>]*?>)[\s\S]*?<\/code>|<script(?:>|\s[^<>]*?>)[\s\S]*?<\/script>|<style(?:>|\s[^<>]*?>)[\s\S]*?<\/style>|<textarea(?:>|\s[^<>]*?>)[\s\S]*?<\/textarea>)#i';
    $input = preg_split($split, $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
    $output = "";
    foreach($input as $v) {
        if( ! trim($v)) {
            $output .= $v;
            continue;
        }
        if($v[0] === '<' && substr($v, -1) === '>') {
            // this is a HTML tag ...
            $output .= $v;
        } else {
            // process ...
            $output .= do_link_auto($v);
        }
    }
    return $output;
}

function do_link_auto_filter_x($text) {
    return preg_replace_callback('#\[([^][]*?)\]\[#', function($m) {
        return '[' . htmlspecialchars($m[1]) . '][';
    }, $text);
}

// `do_parse_markdown` filter is on priority `1`
Filter::add(array('content', 'message'), 'do_link_auto_filter_x', 1.1);
Filter::add(array('content', 'message'), 'do_link_auto_filter', 1.11);

Basically, the snippet above will convert any referenced link syntax in your post content into inline HTML link with URL and title data that is generated automatically based on the given post slug:

[text][article:foo-bar] → <a href="http://mecha-cms.com/2016/article/foo-bar" title="Title">text</a>
[article:foo-bar] → <a href="http://mecha-cms.com/2016/article/foo-bar" title="Title">Title</a>
[][article:foo-bar] → <a href="http://mecha-cms.com/2016/article/foo-bar" title="Title">Title</a>

The advantage of using this syntax is that your Markdown source code will be clean, and since you are using the original Markdown syntax implementation, your source code will be valid and easy to maintain. 1

The Editor

This text editor button was adapted from the smart internal link plugin’s editor button to be able to output smart internal link syntax in Markdown format:

if($config->page_type === 'manager') {
    $posts = array();
    foreach(glob(POST . DS . '*', GLOB_NOSORT | GLOB_ONLYDIR) as $k => $v) {
        $s = File::B($v);
        $posts[$s] = isset($speak->{$s}) ? $speak->{$s} : Text::parse($s, '->title');
    }
    Config::merge('DASHBOARD.languages.MTE.smart_internal_link', $posts);
    Weapon::add('SHIPMENT_REGION_BOTTOM', function() {
        echo '<script>!function(e,n,t){if(t.composer){var r=t.languages.MTE,o="book",i=["Smart Internal Link","⌘+⇧+L"];t.composer.button(o,{title:i,click:function(o,a){a.modal("smart-internal-link",function(o,c,l,u,s){var p=a.grip.selection(),d=n.createElement("button"),f=n.createElement("button"),v=n.createElement("input"),g=n.createElement("select"),m=r.smart_internal_link;for(var k in m)g.innerHTML+=\'<option value="\'+k+\'"\'+(k===t.segment?" selected":"")+">"+m[k]+"</option>";v.type="text",v.placeholder=t.task.slug(i[0].toLowerCase()),l.innerHTML=i[0],u.appendChild(v),u.appendChild(g),d.innerHTML=r.actions.ok,f.innerHTML=r.actions.cancel;var w=function(){var e=v.value,n=/^(.*?)([?&#])(.*?)$/.exec(e);if(!e.length)return!1;n&&n[1]?(n[1]=t.task.slug(n[1].toLowerCase()),e=n[1]+n[2]+n[3]):e=t.task.slug(e.toLowerCase());var r="["+g.value+":"+e+"]";return a.grip.tidy(" ",function(){p.value.length?a.grip.wrap("[","]"+r):a.grip.insert(r)}),!1};a.event("keydown",v,function(e){var n=a.grip.key(e);return"enter"===n?w():"escape"===n?(a.exit(!0),!1):"arrowdown"===n?(d.focus(),!1):void 0}),a.event("click",d,w),a.event("click",f,function(){return a.exit(!0),!1}),a.event("keydown",d,function(e){var n=a.grip.key(e);return"enter"===n?w():"escape"===n?(a.exit(!0),!1):"arrowup"===n?(v.focus(),!1):n.match(/^arrow(right|down)$/)?(f.focus(),!1):void 0}),a.event("keydown",f,function(e){var n=a.grip.key(e);return n.match(/^enter|escape$/)?(a.exit(!0),!1):n.match(/^arrow(left|up)$/)?(d.focus(),!1):"arrowdown"===n?!1:void 0}),s.appendChild(d),s.appendChild(f),e.setTimeout(function(){v.focus(),v.select()},.2)})}}),t.composer.shortcut("ctrl+shift+l",function(){return t.composer.grip.config.buttons[o].click(null,t.composer),!1})}}(window,document,DASHBOARD);</script>';
    }, 20);
}

The Syntax

This is some text with an [automatic referenced link][article:foo-bar]. You can add referenced link syntax without any text in it [][article:foo-bar] or even without the first surrounding square bracket like [article:foo-bar]. So that the post title will be used in place of the link text.

Hash and query string URL can also be included [like so][article:foo?bar=baz#qux].

Reference ID without colon will be ignored by the converter like [so][foo-bar]. That’s why you can still able to set your own reference link value normally.

[foo-bar]: http://example.com/foo-bar "Like this!"

The Instruction

  • Copy the converter and editor code snippet above then put them in your functions.php file
  • Update your old smart internal link’s syntax
  • Remove the smart internal link plugin.

  1. You can just remove the syntax converter above and add some predefined links to re–enable the referenced links syntax using the built-in Markdown parser feature. 

Donation and Email Subscription